commit 981e1a663c729f8e878260798db96d9f557fccd7 Author: Alessio Date: Sat Nov 9 19:50:05 2024 -0800 Initial commit diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..a31d0ca --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,90 @@ +name: Build + +on: + push: + branches: ["*"] # Any branch + tags: ["v*.*.*"] # Release tags + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: "1.22" + + - name: Install dependencies + run: | + go mod download + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.59.1 + + - name: Lint + run: golangci-lint run + + - name: Validate SQL schema + run: | + sqlite3 whatever.db < pkg/db/schema.sql + + - name: Run tests + run: | + mkdir -p sample_data + go test ./... + + release: + runs-on: ubuntu-latest + needs: build # Only run if build is successful + if: startsWith(github.ref, 'refs/tags/v') + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: "1.22" + + - name: Install dependencies + run: | + go mod download + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.59.1 + + - name: Install musl + run: | + sudo apt update + sudo apt install -y musl-tools musl-dev + + - name: Compile with musl + env: + CC: musl-gcc # Use musl-gcc as the C compiler + GOOS: linux + CGO_ENABLED: 1 + GOARCH: amd64 + run: | + go build -v -ldflags '-s -w -linkmode external -extldflags "-static"' -o azm ./cmd + + - name: Create release + id: create_release + uses: actions/create-release@v1 + with: + tag_name: ${{ github.ref_name }} # The tag that triggered the workflow + release_name: Release ${{ github.ref_name }} # Name of the release + draft: true # Set to true if you want to create a draft release + prerelease: true # Set to true if it's a prerelease + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload Release Assets + uses: actions/upload-release-asset@v1 + with: + upload_url: ${{ steps.create_release.outputs.upload_url }} # URL to upload assets + asset_path: azm # Path to your built artifact(s) + asset_name: azm # Name of the asset + asset_content_type: application/octet-stream + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bc31e77 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +sample_data/data diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..d76e8ce --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,659 @@ +# This file contains all available configuration options +# with their default values. + +# output configuration options +output: + # colored-line-number|line-number|json|tab|checkstyle|code-climate|junit-xml|github-actions + formats: colored-line-number + + # sorts results by: filepath, line and column + sort-results: true + +linters: + disable-all: true + enable: + - depguard + - errcheck + - gosimple + - govet + - ineffassign + - staticcheck + - typecheck + - unused + - whitespace + - wrapcheck + - lll + - godox + - gofmt + - errorlint + - nolintlint + - sqlclosecheck + + + # Useless linters: + # - dogsled + # - wsl (don't like it for now) + # - golint (deprecated, replaced by 'revive') + +# TODO: "go fix" -- what is it? What does it do? + + +linters-settings: +# cyclop: +# # the maximal code complexity to report +# max-complexity: 10 +# # the maximal average package complexity. If it's higher than 0.0 (float) the check is enabled (default 0.0) +# package-average: 0.0 +# # should ignore tests (default false) +# skip-tests: false + +# dupl: +# # tokens count to trigger issue, 150 by default +# threshold: 100 + + errcheck: + # report about not checking of errors in type assertions: `a := b.(MyStruct)`; + # default is false: such cases aren't reported by default. + check-type-assertions: true + + # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`; + # default is false: such cases aren't reported by default. + check-blank: true + + # # list of functions to exclude from checking, where each entry is a single function to exclude. + # # see https://github.com/kisielk/errcheck#excluding-functions for details + # exclude-functions: + # - io/ioutil.ReadFile + # - io.Copy(*bytes.Buffer) + # - io.Copy(os.Stdout) + + errorlint: + errorf: true # Ensure Errorf only uses %w (not %v or %s etc) for errors + asserts: true # Require errors.As instead of type-asserting + comparison: true # Require errors.Is instead of equality-checking + +# exhaustive: +# # check switch statements in generated files also +# check-generated: false +# # indicates that switch statements are to be considered exhaustive if a +# # 'default' case is present, even if all enum members aren't listed in the +# # switch +# default-signifies-exhaustive: false + +# exhaustivestruct: +# # Struct Patterns is list of expressions to match struct packages and names +# # The struct packages have the form example.com/package.ExampleStruct +# # The matching patterns can use matching syntax from https://pkg.go.dev/path#Match +# # If this list is empty, all structs are tested. +# struct-patterns: +# - '*.Test' +# - 'example.com/package.ExampleStruct' + +# forbidigo: +# # Forbid the following identifiers (identifiers are written using regexp): +# forbid: +# - ^print.*$ +# - 'fmt\.Print.*' +# # Exclude godoc examples from forbidigo checks. Default is true. +# exclude_godoc_examples: false + +# gci: +# # put imports beginning with prefix after 3rd-party packages; +# # only support one prefix +# # if not set, use goimports.local-prefixes +# local-prefixes: github.com/org/project + +# gocognit: +# # minimal code complexity to report, 30 by default (but we recommend 10-20) +# min-complexity: 10 + +# goconst: +# # minimal length of string constant, 3 by default +# min-len: 3 +# # minimum occurrences of constant string count to trigger issue, 3 by default +# min-occurrences: 3 +# # ignore test files, false by default +# ignore-tests: false +# # look for existing constants matching the values, true by default +# match-constant: true +# # search also for duplicated numbers, false by default +# numbers: false +# # minimum value, only works with goconst.numbers, 3 by default +# min: 3 +# # maximum value, only works with goconst.numbers, 3 by default +# max: 3 +# # ignore when constant is not used as function argument, true by default +# ignore-calls: true + +# gocritic: +# # Which checks should be enabled; can't be combined with 'disabled-checks'; +# # See https://go-critic.github.io/overview#checks-overview +# # To check which checks are enabled run `GL_DEBUG=gocritic golangci-lint run` +# # By default list of stable checks is used. +# enabled-checks: +# - rangeValCopy + +# # Which checks should be disabled; can't be combined with 'enabled-checks'; default is empty +# disabled-checks: +# - regexpMust + +# # Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint run` to see all tags and checks. +# # Empty list by default. See https://github.com/go-critic/go-critic#usage -> section "Tags". +# enabled-tags: +# - performance +# disabled-tags: +# - experimental + +# # Settings passed to gocritic. +# # The settings key is the name of a supported gocritic checker. +# # The list of supported checkers can be find in https://go-critic.github.io/overview. +# settings: +# captLocal: # must be valid enabled check name +# # whether to restrict checker to params only (default true) +# paramsOnly: true +# elseif: +# # whether to skip balanced if-else pairs (default true) +# skipBalanced: true +# hugeParam: +# # size in bytes that makes the warning trigger (default 80) +# sizeThreshold: 80 +# nestingReduce: +# # min number of statements inside a branch to trigger a warning (default 5) +# bodyWidth: 5 +# rangeExprCopy: +# # size in bytes that makes the warning trigger (default 512) +# sizeThreshold: 512 +# # whether to check test functions (default true) +# skipTestFuncs: true +# rangeValCopy: +# # size in bytes that makes the warning trigger (default 128) +# sizeThreshold: 32 +# # whether to check test functions (default true) +# skipTestFuncs: true +# ruleguard: +# # path to a gorules file for the ruleguard checker +# rules: '' +# truncateCmp: +# # whether to skip int/uint/uintptr types (default true) +# skipArchDependent: true +# underef: +# # whether to skip (*x).method() calls where x is a pointer receiver (default true) +# skipRecvDeref: true +# unnamedResult: +# # whether to check exported functions +# checkExported: true + +# gocyclo: +# # minimal code complexity to report, 30 by default (but we recommend 10-20) +# min-complexity: 10 + +# godot: +# # comments to be checked: `declarations`, `toplevel`, or `all` +# scope: declarations +# # list of regexps for excluding particular comment lines from check +# exclude: +# # example: exclude comments which contain numbers +# # - '[0-9]+' +# # check that each sentence starts with a capital letter +# capital: false + + godox: + # report any comments starting with keywords, this is useful for TODO or FIXME comments that + # might be left in the code accidentally and should be resolved before merging + keywords: # default keywords are TODO, BUG, and FIXME, these can be overwritten by this setting + - XXX + + gofmt: + # simplify code: gofmt with `-s` option, true by default + simplify: true + +# gofumpt: +# # Select the Go version to target. The default is `1.15`. +# lang-version: "1.15" + +# # Choose whether or not to use the extra rules that are disabled +# # by default +# extra-rules: false + +# goheader: +# values: +# const: +# # define here const type values in format k:v, for example: +# # COMPANY: MY COMPANY +# regexp: +# # define here regexp type values, for example +# # AUTHOR: .*@mycompany\.com +# template: # |- +# # put here copyright header template for source code files, for example: +# # Note: {{ YEAR }} is a builtin value that returns the year relative to the current machine time. +# # +# # {{ AUTHOR }} {{ COMPANY }} {{ YEAR }} +# # SPDX-License-Identifier: Apache-2.0 + +# # Licensed under the Apache License, Version 2.0 (the "License"); +# # you may not use this file except in compliance with the License. +# # You may obtain a copy of the License at: + +# # http://www.apache.org/licenses/LICENSE-2.0 + +# # Unless required by applicable law or agreed to in writing, software +# # distributed under the License is distributed on an "AS IS" BASIS, +# # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# # See the License for the specific language governing permissions and +# # limitations under the License. +# template-path: +# # also as alternative of directive 'template' you may put the path to file with the template source + +# goimports: +# # put imports beginning with prefix after 3rd-party packages; +# # it's a comma-separated list of prefixes + +# gomnd: +# settings: +# mnd: +# # the list of enabled checks, see https://github.com/tommy-muehle/go-mnd/#checks for description. +# checks: argument,case,condition,operation,return,assign +# # ignored-numbers: 1000 +# # ignored-files: magic_.*.go +# # ignored-functions: math.* + +# gomoddirectives: +# # Allow local `replace` directives. Default is false. +# replace-local: false +# # List of allowed `replace` directives. Default is empty. +# replace-allow-list: +# - launchpad.net/gocheck +# # Allow to not explain why the version has been retracted in the `retract` directives. Default is false. +# retract-allow-no-explanation: false +# # Forbid the use of the `exclude` directives. Default is false. +# exclude-forbidden: false + +# gomodguard: +# allowed: +# modules: # List of allowed modules +# # - gopkg.in/yaml.v2 +# domains: # List of allowed module domains +# # - golang.org +# blocked: +# modules: # List of blocked modules +# # - github.com/uudashr/go-module: # Blocked module +# # recommendations: # Recommended modules that should be used instead (Optional) +# # - golang.org/x/mod +# # reason: "`mod` is the official go.mod parser library." # Reason why the recommended module should be used (Optional) +# versions: # List of blocked module version constraints +# # - github.com/mitchellh/go-homedir: # Blocked module with version constraint +# # version: "< 1.1.0" # Version constraint, see https://github.com/Masterminds/semver#basic-comparisons +# # reason: "testing if blocked version constraint works." # Reason why the version constraint exists. (Optional) +# local_replace_directives: false # Set to true to raise lint issues for packages that are loaded from a local path via replace directive + +# gosec: +# # To select a subset of rules to run. +# # Available rules: https://github.com/securego/gosec#available-rules +# includes: +# - G401 +# - G306 +# - G101 +# # To specify a set of rules to explicitly exclude. +# # Available rules: https://github.com/securego/gosec#available-rules +# excludes: +# - G204 +# # To specify the configuration of rules. +# # The configuration of rules is not fully documented by gosec: +# # https://github.com/securego/gosec#configuration +# # https://github.com/securego/gosec/blob/569328eade2ccbad4ce2d0f21ee158ab5356a5cf/rules/rulelist.go#L60-L102 +# config: +# G306: "0600" +# G101: +# pattern: "(?i)example" +# ignore_entropy: false +# entropy_threshold: "80.0" +# per_char_threshold: "3.0" +# truncate: "32" + +# gosimple: +# # Select the Go version to target. The default is '1.13'. +# go: "1.15" +# # https://staticcheck.io/docs/options#checks +# checks: [ "all" ] + + govet: + # report about shadowed variables + # check-shadowing: true + + # settings per analyzer + # settings: + # printf: # analyzer name, run `go tool vet help` to see all analyzers + # funcs: # run `go tool vet help printf` to see available settings for `printf` analyzer + + # enable or disable analyzers by name + # run `go tool vet help` to see all analyzers + # enable: + # - atomicalign + enable-all: true + # disable-all: false + disable: + - fieldalignment + - composites + - shadow + + + depguard: + # Rules to apply. + # + # Variables: + # - File Variables + # you can still use and exclamation mark ! in front of a variable to say not to use it. + # Example !$test will match any file that is not a go test file. + # + # `$all` - matches all go files + # `$test` - matches all go test files + # + # - Package Variables + # + # `$gostd` - matches all of go's standard library (Pulled from `GOROOT`) + # + # Default: no rules. + rules: + # Name of a rule. + main: + # List of allowed packages. + # allow: + # - $gostd + # - $all + # Packages that are not allowed where the value is a suggestion. + deny: + - pkg: io/ioutil + desc: "replace with the matching functions from `io` or `os` packages" + - pkg: "github.com/pkg/errors" + desc: Should be replaced by standard lib errors package + - pkg: github.com/sirupsen/logrus + desc: "use stdlib logger, just create loggers for each log level that you want" + +# ifshort: +# # Maximum length of variable declaration measured in number of lines, after which linter won't suggest using short syntax. +# # Has higher priority than max-decl-chars. +# max-decl-lines: 1 +# # Maximum length of variable declaration measured in number of characters, after which linter won't suggest using short syntax. +# max-decl-chars: 30 + +# importas: +# # if set to `true`, force to use alias. +# no-unaliased: true +# # List of aliases +# alias: +# # using `servingv1` alias for `knative.dev/serving/pkg/apis/serving/v1` package +# - pkg: knative.dev/serving/pkg/apis/serving/v1 +# alias: servingv1 +# # using `autoscalingv1alpha1` alias for `knative.dev/serving/pkg/apis/autoscaling/v1alpha1` package +# - pkg: knative.dev/serving/pkg/apis/autoscaling/v1alpha1 +# alias: autoscalingv1alpha1 +# # You can specify the package path by regular expression, +# # and alias by regular expression expansion syntax like below. +# # see https://github.com/julz/importas#use-regular-expression for details +# - pkg: knative.dev/serving/pkg/apis/(\w+)/(v[\w\d]+) +# alias: $1$2 + + lll: + # max line length, lines longer will be reported. Default is 120. + # '\t' is counted as 1 character by default, and can be changed with the tab-width option + line-length: 140 + # tab width in spaces. Default to 1. + tab-width: 4 + +# makezero: +# # Allow only slices initialized with a length of zero. Default is false. +# always: false + +# maligned: +# # print struct with more effective memory layout or not, false by default +# suggest-new: true + +# misspell: +# # Correct spellings using locale preferences for US or UK. +# # Default is to use a neutral variety of English. +# # Setting locale to US will correct the British spelling of 'colour' to 'color'. +# locale: US +# ignore-words: +# - someword + +# nakedret: +# # make an issue if func has more lines of code than this setting and it has naked returns; default is 30 +# max-func-lines: 30 + +# prealloc: +# # XXX: we don't recommend using this linter before doing performance profiling. +# # For most programs usage of prealloc will be a premature optimization. + +# # Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them. +# # True by default. +# simple: true +# range-loops: true # Report preallocation suggestions on range loops, true by default +# for-loops: false # Report preallocation suggestions on for loops, false by default + +# promlinter: +# # Promlinter cannot infer all metrics name in static analysis. +# # Enable strict mode will also include the errors caused by failing to parse the args. +# strict: false +# # Please refer to https://github.com/yeya24/promlinter#usage for detailed usage. +# disabled-linters: +# # - "Help" +# # - "MetricUnits" +# # - "Counter" +# # - "HistogramSummaryReserved" +# # - "MetricTypeInName" +# # - "ReservedChars" +# # - "CamelCase" +# # - "lintUnitAbbreviations" + +# predeclared: +# # comma-separated list of predeclared identifiers to not report on +# ignore: "" +# # include method names and field names (i.e., qualified names) in checks +# q: false + + nolintlint: + # Enable to ensure that nolint directives are all used. Default is true. + allow-unused: false + # Disable to ensure that nolint directives don't have a leading space. Default is true. + allow-leading-space: true + # Exclude following linters from requiring an explanation. Default is []. + allow-no-explanation: [] + # Enable to require an explanation of nonzero length after each nolint directive. Default is false. + require-explanation: true + # Enable to require nolint directives to mention the specific linter being suppressed. Default is false. + require-specific: true + +# rowserrcheck: +# packages: +# - github.com/jmoiron/sqlx + +# revive: +# # see https://github.com/mgechev/revive#available-rules for details. +# ignore-generated-header: true +# severity: warning +# rules: +# - name: indent-error-flow +# severity: warning +# - name: add-constant +# severity: warning +# arguments: +# - maxLitCount: "3" +# allowStrs: '""' +# allowInts: "0,1,2" +# allowFloats: "0.0,0.,1.0,1.,2.0,2." + +# staticcheck: +# # Select the Go version to target. The default is '1.13'. +# go: "1.15" +# # https://staticcheck.io/docs/options#checks +# checks: [ "all" ] + +# stylecheck: +# # Select the Go version to target. The default is '1.13'. +# go: "1.15" +# # https://staticcheck.io/docs/options#checks +# checks: [ "all", "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", "-ST1022" ] +# # https://staticcheck.io/docs/options#dot_import_whitelist +# dot-import-whitelist: +# - fmt +# # https://staticcheck.io/docs/options#initialisms +# initialisms: [ "ACL", "API", "ASCII", "CPU", "CSS", "DNS", "EOF", "GUID", "HTML", "HTTP", "HTTPS", "ID", "IP", "JSON", "QPS", "RAM", "RPC", "SLA", "SMTP", "SQL", "SSH", "TCP", "TLS", "TTL", "UDP", "UI", "GID", "UID", "UUID", "URI", "URL", "UTF8", "VM", "XML", "XMPP", "XSRF", "XSS" ] +# # https://staticcheck.io/docs/options#http_status_code_whitelist +# http-status-code-whitelist: [ "200", "400", "404", "500" ] + +# tagliatelle: +# # check the struck tag name case +# case: +# # use the struct field name to check the name of the struct tag +# use-field-name: true +# rules: +# # any struct tag type can be used. +# # support string case: `camel`, `pascal`, `kebab`, `snake`, `goCamel`, `goPascal`, `goKebab`, `goSnake`, `upper`, `lower` +# json: camel +# yaml: camel +# xml: camel +# bson: camel +# avro: snake +# mapstructure: kebab + +# testpackage: +# # regexp pattern to skip files +# skip-regexp: (export|internal)_test\.go + +# thelper: +# # The following configurations enable all checks. It can be omitted because all checks are enabled by default. +# # You can enable only required checks deleting unnecessary checks. +# test: +# first: true +# name: true +# begin: true +# benchmark: +# first: true +# name: true +# begin: true +# tb: +# first: true +# name: true +# begin: true + +# unparam: +# # Inspect exported functions, default is false. Set to true if no external program/library imports your code. +# # XXX: if you enable this setting, unparam will report a lot of false-positives in text editors: +# # if it's called for subdir of a project it can't find external interfaces. All text editor integrations +# # with golangci-lint call it on a directory with the changed file. +# check-exported: false + +# unused: +# # Select the Go version to target. The default is '1.13'. +# go: "1.15" + +# whitespace: +# multi-if: false # Enforces newlines (or comments) after every multi-line if statement +# multi-func: false # Enforces newlines (or comments) after every multi-line function signature + + wrapcheck: + # An array of strings that specify substrings of signatures to ignore. + # If this set, it will override the default set of ignored signatures. + # See https://github.com/tomarrell/wrapcheck#configuration for more information. + ignoreSigs: + +# # The custom section can be used to define linter plugins to be loaded at runtime. +# # See README doc for more info. +# custom: +# # Each custom linter should have a unique name. +# example: +# # The path to the plugin *.so. Can be absolute or local. Required for each custom linter +# path: /path/to/example.so +# # The description of the linter. Optional, just for documentation purposes. +# description: This is an example usage of a plugin linter. +# # Intended to point to the repo location of the linter. Optional, just for documentation purposes. +# original-url: github.com/golangci/example-linter + + +# issues: +# # List of regexps of issue texts to exclude, empty list by default. +# # But independently from this option we use default exclude patterns, +# # it can be disabled by `exclude-use-default: false`. To list all +# # excluded by default patterns execute `golangci-lint run --help` +# exclude: +# - abcdef + +# # Excluding configuration per-path, per-linter, per-text and per-source +# exclude-rules: +# # Exclude some linters from running on tests files. +# - path: _test\.go +# linters: +# - gocyclo +# - errcheck +# - dupl +# - gosec + +# # Exclude known linters from partially hard-vendored code, +# # which is impossible to exclude via "nolint" comments. +# - path: internal/hmac/ +# text: "weak cryptographic primitive" +# linters: +# - gosec + +# # Exclude some staticcheck messages +# - linters: +# - staticcheck +# text: "SA9003:" + +# # Exclude lll issues for long lines with go:generate +# - linters: +# - lll +# source: "^//go:generate " + +# # Independently from option `exclude` we use default exclude patterns, +# # it can be disabled by this option. To list all +# # excluded by default patterns execute `golangci-lint run --help`. +# # Default value for this option is true. +# exclude-use-default: false + +# # The default value is false. If set to true exclude and exclude-rules +# # regular expressions become case sensitive. +# exclude-case-sensitive: false + +# # The list of ids of default excludes to include or disable. By default it's empty. +# include: +# - EXC0002 # disable excluding of issues about comments from golint + +# # Maximum issues count per one linter. Set to 0 to disable. Default is 50. +# max-issues-per-linter: 0 + +# # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. +# max-same-issues: 0 + +# # Show only new issues: if there are unstaged changes or untracked files, +# # only those changes are analyzed, else only changes in HEAD~ are analyzed. +# # It's a super-useful option for integration of golangci-lint into existing +# # large codebase. It's not practical to fix all existing issues at the moment +# # of integration: much better don't allow issues in new code. +# # Default is false. +# new: false + +# severity: +# # Default value is empty string. +# # Set the default severity for issues. If severity rules are defined and the issues +# # do not match or no severity is provided to the rule this will be the default +# # severity applied. Severities should match the supported severity names of the +# # selected out format. +# # - Code climate: https://docs.codeclimate.com/docs/issues#issue-severity +# # - Checkstyle: https://checkstyle.sourceforge.io/property_types.html#severity +# # - Github: https://help.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message +# default-severity: error + +# # The default value is false. +# # If set to true severity-rules regular expressions become case sensitive. +# case-sensitive: false + +# # Default value is empty list. +# # When a list of severity rules are provided, severity information will be added to lint +# # issues. Severity rules have the same filtering capability as exclude rules except you +# # are allowed to specify one matcher per severity rule. +# # Only affects out formats that support setting severity information. +# rules: +# - linters: +# - dupl +# severity: info diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..6fa22c8 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,57 @@ +package main + +import ( + "flag" + "fmt" + "os" + "path/filepath" + + pkg_db "recipe_book/pkg/db" +) + +const DB_FILENAME = "food.db" + +var db_path string = "" + +func main() { + flag.StringVar(&db_path, "db", "sample_data/data", "database path") + + flag.Parse() + args := flag.Args() + + if len(args) == 0 { + fmt.Printf("subcommand needed\n") + os.Exit(1) + } + + switch args[0] { + case "init": + init_db() + default: + fmt.Printf(COLOR_RED+"invalid subcommand: %q\n"+COLOR_RESET, args[0]) + os.Exit(1) + } +} + +func init_db() { + db_filename := filepath.Join(db_path, DB_FILENAME) + _, err := pkg_db.DBCreate(db_filename) + if err != nil { + fmt.Println(COLOR_RED + err.Error() + COLOR_RESET) + os.Exit(1) + } + fmt.Println(COLOR_GREEN + "Successfully created the db" + COLOR_RESET) +} + +const ( + COLOR_RESET = "\033[0m" + COLOR_BLACK = "\033[30m" + COLOR_RED = "\033[31m" + COLOR_GREEN = "\033[32m" + COLOR_YELLOW = "\033[33m" + COLOR_BLUE = "\033[34m" + COLOR_PURPLE = "\033[35m" + COLOR_CYAN = "\033[36m" + COLOR_GRAY = "\033[37m" + COLOR_WHITE = "\033[97m" +) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8eb52f3 --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +module recipe_book + +go 1.22.5 + +require ( + github.com/go-test/deep v1.1.1 + github.com/jmoiron/sqlx v1.4.0 + github.com/mattn/go-sqlite3 v1.14.24 + github.com/stretchr/testify v1.9.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..fa0fc7b --- /dev/null +++ b/go.sum @@ -0,0 +1,23 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= +github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= +github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/db/db_connect.go b/pkg/db/db_connect.go new file mode 100644 index 0000000..bbe5157 --- /dev/null +++ b/pkg/db/db_connect.go @@ -0,0 +1,122 @@ +package db + +import ( + _ "embed" + "errors" + "fmt" + "os" + + "github.com/jmoiron/sqlx" + _ "github.com/mattn/go-sqlite3" +) + +//go:embed schema.sql +var sql_schema string + +// Database starts at version 0. First migration brings us to version 1 +var MIGRATIONS = []string{} +var ENGINE_DATABASE_VERSION = len(MIGRATIONS) + +var ( + ErrTargetExists = errors.New("target already exists") +) + +type DB struct { + DB *sqlx.DB +} + +func DBCreate(path string) (DB, error) { + // First check if the path already exists + _, err := os.Stat(path) + if err == nil { + return DB{}, ErrTargetExists + } else if !errors.Is(err, os.ErrNotExist) { + return DB{}, fmt.Errorf("path error: %w", err) + } + + // Create DB file + fmt.Printf("Creating.............%s\n", path) + db := sqlx.MustOpen("sqlite3", path+"?_foreign_keys=on&_journal_mode=WAL") + db.MustExec(sql_schema) + + return DB{db}, nil +} + +func DBConnect(path string) (DB, error) { + db := sqlx.MustOpen("sqlite3", fmt.Sprintf("%s?_foreign_keys=on&_journal_mode=WAL", path)) + ret := DB{db} + err := ret.CheckAndUpdateVersion() + return ret, err +} + +/** + * Colors for terminal output + */ +const ( + COLOR_RESET = "\033[0m" + COLOR_BLACK = "\033[30m" + COLOR_RED = "\033[31m" + COLOR_GREEN = "\033[32m" + COLOR_YELLOW = "\033[33m" + COLOR_BLUE = "\033[34m" + COLOR_PURPLE = "\033[35m" + COLOR_CYAN = "\033[36m" + COLOR_GRAY = "\033[37m" + COLOR_WHITE = "\033[97m" +) + +func (db DB) CheckAndUpdateVersion() error { + var version int + err := db.DB.Get(&version, "select version from db_version") + if err != nil { + return fmt.Errorf("couldn't check database version: %w", err) + } + + if version > ENGINE_DATABASE_VERSION { + return VersionMismatchError{ENGINE_DATABASE_VERSION, version} + } + + if ENGINE_DATABASE_VERSION > version { + fmt.Print(COLOR_YELLOW) + fmt.Printf("================================================\n") + fmt.Printf("Database version is out of date. Upgrading database from version %d to version %d!\n", version, + ENGINE_DATABASE_VERSION) + fmt.Print(COLOR_RESET) + db.UpgradeFromXToY(version, ENGINE_DATABASE_VERSION) + } + + return nil +} + +// Run all the migrations from version X to version Y, and update the `database_version` table's `version_number` +func (db DB) UpgradeFromXToY(x int, y int) { + for i := x; i < y; i++ { + fmt.Print(COLOR_CYAN) + fmt.Println(MIGRATIONS[i]) + fmt.Print(COLOR_RESET) + + db.DB.MustExec(MIGRATIONS[i]) + db.DB.MustExec("update db_version set version = ?", i+1) + + fmt.Print(COLOR_YELLOW) + fmt.Printf("Now at database schema version %d.\n", i+1) + fmt.Print(COLOR_RESET) + } + fmt.Print(COLOR_GREEN) + fmt.Printf("================================================\n") + fmt.Printf("Database version has been upgraded to version %d.\n", y) + fmt.Print(COLOR_RESET) +} + +type VersionMismatchError struct { + EngineVersion int + DatabaseVersion int +} + +func (e VersionMismatchError) Error() string { + return fmt.Sprintf( + `This profile was created with database schema version %d, which is newer than this application's database schema version, %d. +Please upgrade this application to a newer version to use this profile. Or downgrade the profile's schema version, somehow.`, + e.DatabaseVersion, e.EngineVersion, + ) +} diff --git a/pkg/db/db_connect_test.go b/pkg/db/db_connect_test.go new file mode 100644 index 0000000..24e1fe4 --- /dev/null +++ b/pkg/db/db_connect_test.go @@ -0,0 +1,33 @@ +package db_test + +import ( + "errors" + "fmt" + "math/rand" + "testing" + + "github.com/stretchr/testify/assert" + + . "recipe_book/pkg/db" +) + +func get_test_db() DB { + db_path := "../../sample_data/data/test.db" + db, err := DBCreate(db_path) + if errors.Is(err, ErrTargetExists) { + db, err = DBConnect(db_path) + } + if err != nil { + panic(err) + } + return db +} + +func TestCreateAndConnectToDB(t *testing.T) { + i := rand.Uint32() + _, err := DBCreate(fmt.Sprintf("../../sample_data/data/random-%d.db", i)) + assert.NoError(t, err) + + _, err = DBConnect(fmt.Sprintf("../../sample_data/data/random-%d.db", i)) + assert.NoError(t, err) +} diff --git a/pkg/db/food.go b/pkg/db/food.go new file mode 100644 index 0000000..15eb7c1 --- /dev/null +++ b/pkg/db/food.go @@ -0,0 +1,105 @@ +package db + +import ( + "fmt" +) + +type FoodID uint64 + +type Food struct { + ID FoodID `db:"rowid"` + Name string `db:"name"` + + Cals float32 `db:"cals"` + Carbs float32 `db:"carbs"` + Protein float32 `db:"protein"` + Fat float32 `db:"fat"` + Sugar float32 `db:"sugar"` + Alcohol float32 `db:"alcohol"` + + Water float32 `db:"water"` + + Potassium float32 `db:"potassium"` + Calcium float32 `db:"calcium"` + Sodium float32 `db:"sodium"` + Magnesium float32 `db:"magnesium"` + Phosphorus float32 `db:"phosphorus"` + Iron float32 `db:"iron"` + Zinc float32 `db:"zinc"` + + Mass float32 `db:"mass"` + Price float32 `db:"price"` + Density float32 `db:"density"` + CookRatio float32 `db:"cook_ratio"` +} + +// Format as string +func (f Food) String() string { + return fmt.Sprintf("%s(%d)", f.Name, f.ID) +} + +func (db *DB) SaveFood(f *Food) { + if f.ID == FoodID(0) { + // Do create + result, err := db.DB.NamedExec(` + insert into foods (name, cals, carbs, protein, fat, sugar, alcohol, water, potassium, calcium, sodium, + magnesium, phosphorus, iron, zinc, mass, price, density, cook_ratio) + values (:name, :cals, :carbs, :protein, :fat, :sugar, :alcohol, :water, :potassium, :calcium, + :sodium, :magnesium, :phosphorus, :iron, :zinc, :mass, :price, :density, :cook_ratio) + `, f) + if err != nil { + panic(err) + } + // Update the ID if necessary + id, err := result.LastInsertId() + if err != nil { + panic(err) + } + f.ID = FoodID(id) + } else { + // Do update + result, err := db.DB.NamedExec(` + update foods + set name=:name, + cals=:cals, + carbs=:carbs, + protein=:protein, + fat=:fat, + sugar=:sugar, + alcohol=:alcohol, + water=:water, + potassium=:potassium, + calcium=:calcium, + sodium=:sodium, + magnesium=:magnesium, + phosphorus=:phosphorus, + iron=:iron, + zinc=:zinc, + mass=:mass, + price=:price, + density=:density, + cook_ratio=:cook_ratio + where rowid = :rowid + `, f) + if err != nil { + panic(err) + } + count, err := result.RowsAffected() + if err != nil { + panic(err) + } + if count != 1 { + panic(fmt.Errorf("Got food with ID (%d), so attempted update, but it doesn't exist", f.ID)) + } + } +} + +func (db *DB) GetFoodByID(id FoodID) (ret Food, err error) { + err = db.DB.Get(&ret, ` + select rowid, name, cals, carbs, protein, fat, sugar, alcohol, water, potassium, calcium, sodium, + magnesium, phosphorus, iron, zinc, mass, price, density, cook_ratio + from foods + where rowid = ? + `, id) + return +} diff --git a/pkg/db/food_test.go b/pkg/db/food_test.go new file mode 100644 index 0000000..b831ece --- /dev/null +++ b/pkg/db/food_test.go @@ -0,0 +1,72 @@ +package db_test + +import ( + "testing" + + "github.com/go-test/deep" + "github.com/stretchr/testify/assert" + + . "recipe_book/pkg/db" +) + +func TestFoodSaveAndLoad(t *testing.T) { + assert := assert.New(t) + db := get_test_db() + food := Food{ + Name: "some food", + Cals: 1.0, + Carbs: 2.0, + Protein: 3.0, + Fat: 4.0, + Sugar: 5.0, + Alcohol: 6.0, + Water: 7.0, + Potassium: 8.0, + Calcium: 9.0, + Sodium: 10.0, + Magnesium: 11.0, + Phosphorus: 12.0, + Iron: 13.0, + Zinc: 14.0, + Mass: 15.0, + Price: 16.0, + Density: 17.0, + CookRatio: 18.0, + } + assert.Equal(food.ID, FoodID(0)) + db.SaveFood(&food) + assert.NotEqual(food.ID, FoodID(0)) + new_food, err := db.GetFoodByID(food.ID) + assert.NoError(err) + if diff := deep.Equal(food, new_food); diff != nil { + t.Error(diff) + } + + // Modify it + food.Name = "another food" + food.Cals = food.Cals + 9.2 + food.Carbs = food.Carbs + 9.2 + food.Protein = food.Protein + 9.2 + food.Fat = food.Fat + 9.2 + food.Sugar = food.Sugar + 9.2 + food.Alcohol = food.Alcohol + 9.2 + food.Water = food.Water + 9.2 + food.Potassium = food.Potassium + 9.2 + food.Calcium = food.Calcium + 9.2 + food.Sodium = food.Sodium + 9.2 + food.Phosphorus = food.Phosphorus + 9.2 + food.Iron = food.Iron + 9.2 + food.Zinc = food.Zinc + 9.2 + food.Mass = food.Mass + 9.2 + food.Price = food.Price + 9.2 + food.Density = food.Density + 9.2 + food.CookRatio = food.CookRatio + 9.2 + + // Save it and reload it + db.SaveFood(&food) + new_food, err = db.GetFoodByID(food.ID) + assert.NoError(err) + if diff := deep.Equal(food, new_food); diff != nil { + t.Error(diff) + } +} diff --git a/pkg/db/ingredient.go b/pkg/db/ingredient.go new file mode 100644 index 0000000..febfcb0 --- /dev/null +++ b/pkg/db/ingredient.go @@ -0,0 +1,26 @@ +package db + +import ( +// "fmt" +) + +type IngredientID uint64 + +type Ingredient struct { + ID IngredientID `db:"rowid"` + FoodID FoodID `db:"food_id"` + RecipeID RecipeID `db:"recipe_id"` + + QuantityNumerator int64 `db:"quantity_numerator"` + QuantityDenominator int64 `db:"quantity_denominator"` + Units Units `db:"units"` + + InRecipeID RecipeID `db:"in_recipe_id"` + ListOrder int64 `db:"list_order"` + IsHidden bool `db:"is_hidden"` +} + +// // Format as string +// func (i Ingredient) String() string { +// return fmt.Sprintf("%s(%d)", f.Name, f.ID) +// } diff --git a/pkg/db/recipe.go b/pkg/db/recipe.go new file mode 100644 index 0000000..31f2af9 --- /dev/null +++ b/pkg/db/recipe.go @@ -0,0 +1,103 @@ +package db + +import ( + "database/sql/driver" + "fmt" + "strings" +) + +type RecipeID uint64 + +type RecipeInstructions []string + +// Join the instructions with 0x1F, the "Unit Separator" ASCII character +func (ri RecipeInstructions) Value() (driver.Value, error) { + return strings.Join(ri, "\x1F"), nil +} + +// Split the stored string by "Unit Separator" characters +func (ri *RecipeInstructions) Scan(src interface{}) error { + val, is_ok := src.(string) + if !is_ok { + return fmt.Errorf("incompatible type for RecipeInstructions list: %#v", src) + } + *ri = RecipeInstructions(strings.Split(val, "\x1F")) + return nil +} + +type Recipe struct { + ID RecipeID `db:"rowid"` + Name string `db:"name"` + Blurb string `db:"blurb"` + Instructions RecipeInstructions `db:"instructions"` + + Ingredients []Ingredient + + ComputedFoodID FoodID `db:"computed_food_id"` +} + +func (db *DB) SaveRecipe(r *Recipe) { + if r.ID == RecipeID(0) { + // Do create + result, err := db.DB.NamedExec(` + insert into recipes (name, blurb, instructions) + values (:name, :blurb, :instructions) + on conflict do update + set name=:name, + blurb=:blurb, + instructions=:instructions + `, r) + if err != nil { + panic(err) + } + + // Update the ID + id, err := result.LastInsertId() + if err != nil { + panic(err) + } + r.ID = RecipeID(id) + } else { + // Do update + result, err := db.DB.NamedExec(` + update recipes set name=:name, blurb=:blurb, instructions=:instructions where rowid = :rowid + `, r) + if err != nil { + panic(err) + } + count, err := result.RowsAffected() + if err != nil { + panic(err) + } + if count != 1 { + panic(fmt.Errorf("Got recipe with ID (%d), so attempted update, but it doesn't exist", r.ID)) + } + } + // TODO: recompute the computed_food +} + +// func (db *DB) AddIngredientToRecipe(r Recipe, i *Ingredient) { +// result, err := db.DB.NamedExec(` +// insert into ingredients +// `, ingr) +// } + +func (db *DB) GetRecipeByID(id RecipeID) (ret Recipe, err error) { + err = db.DB.Get(&ret, ` + select rowid, name, blurb, instructions + from recipes + where rowid = ? + `, id) + if err != nil { + return Recipe{}, err + } + + // Load the ingredients + err = db.DB.Select(&ret.Ingredients, ` + select food_id, recipe_id, quantity_numerator, quantity_denominator, units, list_order, is_hidden + from ingredients + where in_recipe_id = ? + order by list_order asc + `, id) + return +} diff --git a/pkg/db/recipe_test.go b/pkg/db/recipe_test.go new file mode 100644 index 0000000..ed17cd0 --- /dev/null +++ b/pkg/db/recipe_test.go @@ -0,0 +1,44 @@ +package db_test + +import ( + "testing" + + "github.com/go-test/deep" + "github.com/stretchr/testify/assert" + + . "recipe_book/pkg/db" +) + +func TestRecipeSaveAndLoad(t *testing.T) { + assert := assert.New(t) + db := get_test_db() + + recipe := Recipe{ + Name: "some Recipe", + Blurb: "Lorem Ispum dolor sit amet consquiter id blah blabh albha blahbla blahblahblh", + Instructions: RecipeInstructions{ + "instr 1", "isntr 2", "instr3", "ins32gjkifw", + }, + } + assert.Equal(recipe.ID, RecipeID(0)) + db.SaveRecipe(&recipe) + assert.NotEqual(recipe.ID, RecipeID(0)) + new_recipe, err := db.GetRecipeByID(recipe.ID) + assert.NoError(err) + if diff := deep.Equal(recipe, new_recipe); diff != nil { + t.Error(diff) + } + + // Modify it + recipe.Name = "some recipe 2" + recipe.Blurb = "another blurb" + recipe.Instructions = RecipeInstructions{"i1", "i2", "i3"} + + // Save it and reload + db.SaveRecipe(&recipe) + new_recipe, err = db.GetRecipeByID(recipe.ID) + assert.NoError(err) + if diff := deep.Equal(recipe, new_recipe); diff != nil { + t.Error(diff) + } +} diff --git a/pkg/db/schema.sql b/pkg/db/schema.sql new file mode 100644 index 0000000..341ce12 --- /dev/null +++ b/pkg/db/schema.sql @@ -0,0 +1,117 @@ +PRAGMA foreign_keys = on; + +-- ======= +-- DB meta +-- ======= + +create table db_version ( + version integer not null +) strict; +insert into db_version values(0); + +create table food_types (rowid integer primary key, + name text not null unique check(length(name) != 0) +); +insert into food_types (name) values + ('grocery'), + ('recipe'), + ('daily log'); + +create table foods (rowid integer primary key, + name text not null check(length(name) != 0), + + -- created_at integer not null, + -- updated_at integer, + + cals real not null, + carbs real not null, + protein real not null, + fat real not null, + + sugar real not null, + alcohol real not null default 0, + water real not null default 0, + + potassium real not null default 0, + sodium real not null default 0, + calcium real not null default 0, + magnesium real not null default 0, + phosphorus real not null default 0, + iron real not null default 0, + zinc real not null default 0, + + mass real not null default 100, + price real not null default 0, + density real not null default -1, + cook_ratio real not null default 1 +) strict; + + +create table units (rowid integer primary key, + name text not null unique check(length(name) != 0), + abbreviation text not null unique check(length(abbreviation) != 0) + -- is_metric integer not null check(is_metric in (0, 1)) +); +insert into units(name, abbreviation) values + -- Count + ('count', 'ct'), + -- Mass + ('grams', 'g'), + ('pounds', 'lbs'), + ('ounces', 'oz'), + -- Volume + ('milliliters', 'mL'), + ('cups', 'cups'), + ('teaspoons', 'tsp'), + ('tablespoons', 'tbsp'), + ('fluid ounces', 'fl-oz'); + + +create table ingredients (rowid integer primary key, + -- created_at integer not null, + -- updated_at integer, + + food_id integer references foods(rowid), + recipe_id integer references recipes(rowid), + + -- Portion size (rational numbers) + quantity_numerator integer not null default 1, + quantity_denominator integer not null default 1, + units integer not null default 0, -- Display purposes only + + in_recipe_id integer references recipes(rowid) on delete cascade not null, + list_order integer not null, + is_hidden integer not null default false, + unique (recipe_id, list_order) + check((food_id is null) + (recipe_id is null) = 1) -- Exactly one should be active +) strict; + +create table recipes (rowid integer primary key, + -- created_at integer not null, + -- updated_at integer, + + name text not null check(length(name) != 0), + blurb text not null, + instructions text not null + + -- computed_food_id integer references foods(rowid) not null +) strict; + +create table iterations (rowid integer primary key, + -- created_at integer not null, + -- updated_at integer, + + original_recipe_id integer references recipes(rowid), + -- original_author integer not null, -- For azimuth integration + derived_recipe_id integer references recipes(rowid), + unique(derived_recipe_id) +) strict; + +create table daily_logs (rowid integer primary key, + -- created_at integer not null, + -- updated_at integer, + + date integer not null unique, + + computed_food_id integer references foods(rowid) not null +); diff --git a/pkg/db/timestamp.go b/pkg/db/timestamp.go new file mode 100644 index 0000000..e827120 --- /dev/null +++ b/pkg/db/timestamp.go @@ -0,0 +1,31 @@ +package db + +import ( + "database/sql/driver" + "fmt" + "time" +) + +type Timestamp struct { + time.Time +} + +func (t Timestamp) Value() (driver.Value, error) { + return t.UnixMilli(), nil +} + +func (t *Timestamp) Scan(src interface{}) error { + val, is_ok := src.(int64) + if !is_ok { + return fmt.Errorf("Incompatible type for Timestamp: %#v", src) + } + *t = Timestamp{time.UnixMilli(val)} + return nil +} + +func TimestampFromUnix(num int64) Timestamp { + return Timestamp{time.Unix(num, 0)} +} +func TimestampFromUnixMilli(num int64) Timestamp { + return Timestamp{time.UnixMilli(num)} +} diff --git a/pkg/db/units.go b/pkg/db/units.go new file mode 100644 index 0000000..de69bc5 --- /dev/null +++ b/pkg/db/units.go @@ -0,0 +1,9 @@ +package db + +type UnitsID uint64 + +type Units struct { + ID UnitsID `db:"rowid"` + Name string `db:"name"` + Abbreviation string `db:"abbreviation"` +} diff --git a/sample_data/mount.sh b/sample_data/mount.sh new file mode 100755 index 0000000..4519630 --- /dev/null +++ b/sample_data/mount.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +mount -t tmpfs -o size=100M tmpfs sample_data/data