From 05395c3e092feae0643a5c2850632f3a4d0caf01 Mon Sep 17 00:00:00 2001 From: Wael Nasreddine Date: Tue, 10 Dec 2024 16:49:37 -0800 Subject: [PATCH] sqlc: define the schema/queries and make use of generated models (#71) Use `sqlc generate` to generate the models and make use of them. The command also generates queries, but they are not being used in this pr to keep it small and manageable. Follow-up PR is coming. --- .env | 1 + .gitignore | 3 + .../20241210054814_create-narinfos-table.sql | 17 ++ .../20241210054829_create-nars-table.sql | 23 ++ db/query.sql | 77 +++++ devbox.json | 4 +- devbox.lock | 96 +++++++ pkg/cache/cache.go | 4 +- pkg/cache/cache_test.go | 38 +-- pkg/database/db.go | 31 ++ pkg/database/models.go | 29 ++ pkg/database/query.sql.go | 271 ++++++++++++++++++ pkg/database/sqlite.go | 53 +--- pkg/database/sqlite_test.go | 58 ++-- sqlc.yml | 15 + 15 files changed, 633 insertions(+), 87 deletions(-) create mode 100644 .env create mode 100644 db/migrations/20241210054814_create-narinfos-table.sql create mode 100644 db/migrations/20241210054829_create-nars-table.sql create mode 100644 db/query.sql create mode 100644 pkg/database/db.go create mode 100644 pkg/database/models.go create mode 100644 pkg/database/query.sql.go create mode 100644 sqlc.yml diff --git a/.env b/.env new file mode 100644 index 0000000..971e5c5 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +DATABASE_URL="sqlite:db/database.sqlite3" diff --git a/.gitignore b/.gitignore index 45490bc..664ea6c 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,6 @@ go.work.sum # Go binaries /ncps + +# SQLite database +/db/database.sqlite3 diff --git a/db/migrations/20241210054814_create-narinfos-table.sql b/db/migrations/20241210054814_create-narinfos-table.sql new file mode 100644 index 0000000..880687f --- /dev/null +++ b/db/migrations/20241210054814_create-narinfos-table.sql @@ -0,0 +1,17 @@ +-- migrate:up +CREATE TABLE narinfos ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + hash TEXT NOT NULL UNIQUE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at TIMESTAMP, + last_accessed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +CREATE UNIQUE INDEX idx_narinfos_id ON narinfos (id); +CREATE UNIQUE INDEX idx_narinfos_hash ON narinfos (hash); +CREATE INDEX idx_narinfos_last_accessed_at ON narinfos (last_accessed_at); + +-- migrate:down +DROP INDEX idx_narinfos_id; +DROP INDEX idx_narinfos_hash; +DROP INDEX idx_narinfos_last_accessed_at; +DROP TABLE narinfos; diff --git a/db/migrations/20241210054829_create-nars-table.sql b/db/migrations/20241210054829_create-nars-table.sql new file mode 100644 index 0000000..c1da2a1 --- /dev/null +++ b/db/migrations/20241210054829_create-nars-table.sql @@ -0,0 +1,23 @@ +-- migrate:up +CREATE TABLE nars ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + narinfo_id INTEGER NOT NULL REFERENCES narinfos(id), + hash TEXT NOT NULL UNIQUE, + compression TEXT NOT NULL DEFAULT '', + file_size INTEGER NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated_at TIMESTAMP, + last_accessed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +CREATE UNIQUE INDEX idx_nars_id ON nars (id); +CREATE UNIQUE INDEX idx_nars_hash ON nars (hash); +CREATE INDEX idx_nars_narinfo_id ON nars (narinfo_id); +CREATE INDEX idx_nars_last_accessed_at ON nars (last_accessed_at); + +-- migrate:down +DROP INDEX idx_nars_id; +DROP INDEX idx_nars_hash; +DROP INDEX idx_nars_narinfo_id; +DROP INDEX idx_nars_last_accessed_at; +DROP TABLE nars; diff --git a/db/query.sql b/db/query.sql new file mode 100644 index 0000000..c2d52e7 --- /dev/null +++ b/db/query.sql @@ -0,0 +1,77 @@ +-- name: GetNarInfoByHash :one +SELECT * +FROM narinfos +WHERE hash = ?; + +-- name: GetNarInfoByID :one +SELECT * +FROM narinfos +WHERE id = ?; + +-- name: GetNarByHash :one +SELECT * +FROM nars +WHERE hash = ?; + +-- name: GetNarByID :one +SELECT * +FROM nars +WHERE id = ?; + +-- name: CreateNarInfo :one +INSERT into narinfos ( + hash +) VALUES ( + ? +) +RETURNING *; + +-- name: CreateNar :one +INSERT into nars ( + narinfo_id, hash, compression, file_size +) VALUES ( + ?, ?, ?, ? +) +RETURNING *; + +-- name: TouchNarInfo :exec +UPDATE narinfos +SET last_accessed_at = CURRENT_TIMESTAMP, + updated_at = CURRENT_TIMESTAMP +WHERE hash = ?; + +-- name: TouchNar :exec +UPDATE nars +SET last_accessed_at = CURRENT_TIMESTAMP, + updated_at = CURRENT_TIMESTAMP +WHERE hash = ?; + +-- name: DeleteNarInfoByHash :exec +DELETE FROM narinfos +WHERE hash = ?; + +-- name: DeleteNarByHash :exec +DELETE FROM nars +WHERE hash = ?; + +-- name: DeleteNarInfoByID :exec +DELETE FROM narinfos +WHERE id = ?; + +-- name: DeleteNarByID :exec +DELETE FROM nars +WHERE id = ?; + +-- name: GetNarTotalSize :one +SELECT SUM(file_size) AS total_size +FROM nars; + +-- name: GetLeastUsedNars :many +SELECT + n1.* +FROM nars n1 +WHERE ( + SELECT SUM(n2.file_size) + FROM nars n2 + WHERE n2.last_accessed_at <= n1.last_accessed_at +) <= ?; diff --git a/devbox.json b/devbox.json index ddc1bec..77a7a11 100644 --- a/devbox.json +++ b/devbox.json @@ -1,8 +1,10 @@ { "$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.13.4/.schema/devbox.schema.json", "packages": [ + "dbmate@2.22", "go@1.23", - "golangci-lint@1.62" + "golangci-lint@1.62", + "sqlc@1.27" ], "shell": { "init_hook": [ diff --git a/devbox.lock b/devbox.lock index c00d849..c058021 100644 --- a/devbox.lock +++ b/devbox.lock @@ -1,6 +1,54 @@ { "lockfile_version": "1", "packages": { + "dbmate@2.22": { + "last_modified": "2024-11-28T07:51:56Z", + "resolved": "github:NixOS/nixpkgs/226216574ada4c3ecefcbbec41f39ce4655f78ef#dbmate", + "source": "devbox-search", + "version": "2.22.0", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/b28qb15ypyj60p4bsv8n2h7dx1rd3i67-dbmate-2.22.0", + "default": true + } + ], + "store_path": "/nix/store/b28qb15ypyj60p4bsv8n2h7dx1rd3i67-dbmate-2.22.0" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/r48rg5qsxv04b6kiyrgz2q9az5vp3dbx-dbmate-2.22.0", + "default": true + } + ], + "store_path": "/nix/store/r48rg5qsxv04b6kiyrgz2q9az5vp3dbx-dbmate-2.22.0" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/4ai029322bsk0vvcbihvp0zymr5hsaq6-dbmate-2.22.0", + "default": true + } + ], + "store_path": "/nix/store/4ai029322bsk0vvcbihvp0zymr5hsaq6-dbmate-2.22.0" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/9mv4isdm26wcy83ia1ga9pdwbch7fvri-dbmate-2.22.0", + "default": true + } + ], + "store_path": "/nix/store/9mv4isdm26wcy83ia1ga9pdwbch7fvri-dbmate-2.22.0" + } + } + }, "go@1.23": { "last_modified": "2024-11-28T07:51:56Z", "resolved": "github:NixOS/nixpkgs/226216574ada4c3ecefcbbec41f39ce4655f78ef#go", @@ -96,6 +144,54 @@ "store_path": "/nix/store/gr55w2x6wrzdvhhbzm4wc28cs4k7g7vr-golangci-lint-1.62.2" } } + }, + "sqlc@1.27": { + "last_modified": "2024-11-28T07:51:56Z", + "resolved": "github:NixOS/nixpkgs/226216574ada4c3ecefcbbec41f39ce4655f78ef#sqlc", + "source": "devbox-search", + "version": "1.27.0", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/nv7n7v77blj22ycymjxr7nbr26gdhfqb-sqlc-1.27.0", + "default": true + } + ], + "store_path": "/nix/store/nv7n7v77blj22ycymjxr7nbr26gdhfqb-sqlc-1.27.0" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/3xklmmk7x08yr31k3wa0hks890sz98r9-sqlc-1.27.0", + "default": true + } + ], + "store_path": "/nix/store/3xklmmk7x08yr31k3wa0hks890sz98r9-sqlc-1.27.0" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/8qhdd418krfkrmczn959m5hv9184b9yb-sqlc-1.27.0", + "default": true + } + ], + "store_path": "/nix/store/8qhdd418krfkrmczn959m5hv9184b9yb-sqlc-1.27.0" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/6mcdgk9rg9g4kia87jjv25h90b2n9r62-sqlc-1.27.0", + "default": true + } + ], + "store_path": "/nix/store/6mcdgk9rg9g4kia87jjv25h90b2n9r62-sqlc-1.27.0" + } + } } } } diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go index 80ee3d9..05708b4 100644 --- a/pkg/cache/cache.go +++ b/pkg/cache/cache.go @@ -330,7 +330,7 @@ func (c *Cache) getNarFromStore(log log15.Logger, hash, compression string) (int return 0, nil, fmt.Errorf("error fetching the nar record: %w", err) } - if time.Since(nr.LastAccessedAt) > c.recordAgeIgnoreTouch { + if lat, err := nr.LastAccessedAt.Value(); err == nil && time.Since(lat.(time.Time)) > c.recordAgeIgnoreTouch { if _, err := c.db.TouchNarRecord(tx, hash); err != nil { return 0, nil, fmt.Errorf("error touching the nar record: %w", err) } @@ -583,7 +583,7 @@ func (c *Cache) getNarInfoFromStore(log log15.Logger, hash string) (*narinfo.Nar return nil, fmt.Errorf("error fetching the narinfo record: %w", err) } - if time.Since(nir.LastAccessedAt) > c.recordAgeIgnoreTouch { + if lat, err := nir.LastAccessedAt.Value(); err == nil && time.Since(lat.(time.Time)) > c.recordAgeIgnoreTouch { if _, err := c.db.TouchNarInfoRecord(tx, hash); err != nil { return nil, fmt.Errorf("error touching the narinfo record: %w", err) } diff --git a/pkg/cache/cache_test.go b/pkg/cache/cache_test.go index bfbcb68..9383145 100644 --- a/pkg/cache/cache_test.go +++ b/pkg/cache/cache_test.go @@ -304,10 +304,10 @@ func TestGetNarInfo(t *testing.T) { rows, err := db.Query(query) require.NoError(t, err) - nims := make([]database.NarInfoModel, 0) + nims := make([]database.NarInfo, 0) for rows.Next() { - var nim database.NarInfoModel + var nim database.NarInfo err := rows.Scan(&nim.Hash, &nim.CreatedAt, &nim.LastAccessedAt) require.NoError(t, err) @@ -319,7 +319,7 @@ func TestGetNarInfo(t *testing.T) { assert.Len(t, nims, 1) assert.Equal(t, testdata.Nar2.NarInfoHash, nims[0].Hash) - assert.Equal(t, nims[0].CreatedAt, nims[0].LastAccessedAt) + assert.Equal(t, nims[0].CreatedAt, nims[0].LastAccessedAt.Time) }) t.Run("nar does exist in the database, and has initial last_accessed_at", func(t *testing.T) { @@ -331,10 +331,10 @@ func TestGetNarInfo(t *testing.T) { rows, err := db.Query(query) require.NoError(t, err) - nims := make([]database.NarModel, 0) + nims := make([]database.Nar, 0) for rows.Next() { - var nim database.NarModel + var nim database.Nar err := rows.Scan( &nim.Hash, @@ -349,7 +349,7 @@ func TestGetNarInfo(t *testing.T) { require.NoError(t, rows.Err()) assert.Len(t, nims, 1) assert.Equal(t, testdata.Nar2.NarHash, nims[0].Hash) - assert.Equal(t, nims[0].CreatedAt, nims[0].LastAccessedAt) + assert.Equal(t, nims[0].CreatedAt, nims[0].LastAccessedAt.Time) }) t.Run("pulling it another time within recordAgeIgnoreTouch should not update last_accessed_at", func(t *testing.T) { @@ -373,10 +373,10 @@ func TestGetNarInfo(t *testing.T) { rows, err := db.Query(query) require.NoError(t, err) - nims := make([]database.NarInfoModel, 0) + nims := make([]database.NarInfo, 0) for rows.Next() { - var nim database.NarInfoModel + var nim database.NarInfo err := rows.Scan(&nim.Hash, &nim.CreatedAt, &nim.LastAccessedAt) require.NoError(t, err) @@ -388,7 +388,7 @@ func TestGetNarInfo(t *testing.T) { assert.Len(t, nims, 1) assert.Equal(t, testdata.Nar2.NarInfoHash, nims[0].Hash) - assert.Equal(t, nims[0].CreatedAt, nims[0].LastAccessedAt) + assert.Equal(t, nims[0].CreatedAt, nims[0].LastAccessedAt.Time) }) }) @@ -407,10 +407,10 @@ func TestGetNarInfo(t *testing.T) { rows, err := db.Query(query) require.NoError(t, err) - nims := make([]database.NarInfoModel, 0) + nims := make([]database.NarInfo, 0) for rows.Next() { - var nim database.NarInfoModel + var nim database.NarInfo err := rows.Scan(&nim.Hash, &nim.CreatedAt, &nim.LastAccessedAt) require.NoError(t, err) @@ -747,10 +747,10 @@ func TestGetNar(t *testing.T) { rows, err := db.Query(query) require.NoError(t, err) - nims := make([]database.NarModel, 0) + nims := make([]database.Nar, 0) for rows.Next() { - var nim database.NarModel + var nim database.Nar err := rows.Scan( &nim.Hash, @@ -766,7 +766,7 @@ func TestGetNar(t *testing.T) { assert.Len(t, nims, 1) assert.Equal(t, testdata.Nar1.NarHash, nims[0].Hash) - assert.Equal(t, nims[0].CreatedAt, nims[0].LastAccessedAt) + assert.Equal(t, nims[0].CreatedAt, nims[0].LastAccessedAt.Time) }) t.Run("pulling it another time within recordAgeIgnoreTouch should not update last_accessed_at", func(t *testing.T) { @@ -791,10 +791,10 @@ func TestGetNar(t *testing.T) { rows, err := db.Query(query) require.NoError(t, err) - nims := make([]database.NarModel, 0) + nims := make([]database.Nar, 0) for rows.Next() { - var nim database.NarModel + var nim database.Nar err := rows.Scan( &nim.Hash, @@ -810,7 +810,7 @@ func TestGetNar(t *testing.T) { assert.Len(t, nims, 1) assert.Equal(t, testdata.Nar1.NarHash, nims[0].Hash) - assert.Equal(t, nims[0].CreatedAt, nims[0].LastAccessedAt) + assert.Equal(t, nims[0].CreatedAt, nims[0].LastAccessedAt.Time) }) }) @@ -830,10 +830,10 @@ func TestGetNar(t *testing.T) { rows, err := db.Query(query) require.NoError(t, err) - nims := make([]database.NarModel, 0) + nims := make([]database.Nar, 0) for rows.Next() { - var nim database.NarModel + var nim database.Nar err := rows.Scan( &nim.Hash, diff --git a/pkg/database/db.go b/pkg/database/db.go new file mode 100644 index 0000000..dacb52e --- /dev/null +++ b/pkg/database/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 + +package database + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/pkg/database/models.go b/pkg/database/models.go new file mode 100644 index 0000000..7469726 --- /dev/null +++ b/pkg/database/models.go @@ -0,0 +1,29 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 + +package database + +import ( + "database/sql" + "time" +) + +type Nar struct { + ID int64 + NarInfoID int64 + Hash string + Compression string + FileSize int64 + CreatedAt time.Time + UpdatedAt sql.NullTime + LastAccessedAt sql.NullTime +} + +type NarInfo struct { + ID int64 + Hash string + CreatedAt time.Time + UpdatedAt sql.NullTime + LastAccessedAt sql.NullTime +} diff --git a/pkg/database/query.sql.go b/pkg/database/query.sql.go new file mode 100644 index 0000000..2aba8f5 --- /dev/null +++ b/pkg/database/query.sql.go @@ -0,0 +1,271 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.27.0 +// source: query.sql + +package database + +import ( + "context" + "database/sql" +) + +const createNar = `-- name: CreateNar :one +INSERT into nars ( + narinfo_id, hash, compression, file_size +) VALUES ( + ?, ?, ?, ? +) +RETURNING id, narinfo_id, hash, compression, file_size, created_at, updated_at, last_accessed_at +` + +type CreateNarParams struct { + NarInfoID int64 + Hash string + Compression string + FileSize int64 +} + +func (q *Queries) CreateNar(ctx context.Context, arg CreateNarParams) (Nar, error) { + row := q.db.QueryRowContext(ctx, createNar, + arg.NarInfoID, + arg.Hash, + arg.Compression, + arg.FileSize, + ) + var i Nar + err := row.Scan( + &i.ID, + &i.NarInfoID, + &i.Hash, + &i.Compression, + &i.FileSize, + &i.CreatedAt, + &i.UpdatedAt, + &i.LastAccessedAt, + ) + return i, err +} + +const createNarInfo = `-- name: CreateNarInfo :one +INSERT into narinfos ( + hash +) VALUES ( + ? +) +RETURNING id, hash, created_at, updated_at, last_accessed_at +` + +func (q *Queries) CreateNarInfo(ctx context.Context, hash string) (NarInfo, error) { + row := q.db.QueryRowContext(ctx, createNarInfo, hash) + var i NarInfo + err := row.Scan( + &i.ID, + &i.Hash, + &i.CreatedAt, + &i.UpdatedAt, + &i.LastAccessedAt, + ) + return i, err +} + +const deleteNarByHash = `-- name: DeleteNarByHash :exec +DELETE FROM nars +WHERE hash = ? +` + +func (q *Queries) DeleteNarByHash(ctx context.Context, hash string) error { + _, err := q.db.ExecContext(ctx, deleteNarByHash, hash) + return err +} + +const deleteNarByID = `-- name: DeleteNarByID :exec +DELETE FROM nars +WHERE id = ? +` + +func (q *Queries) DeleteNarByID(ctx context.Context, id int64) error { + _, err := q.db.ExecContext(ctx, deleteNarByID, id) + return err +} + +const deleteNarInfoByHash = `-- name: DeleteNarInfoByHash :exec +DELETE FROM narinfos +WHERE hash = ? +` + +func (q *Queries) DeleteNarInfoByHash(ctx context.Context, hash string) error { + _, err := q.db.ExecContext(ctx, deleteNarInfoByHash, hash) + return err +} + +const deleteNarInfoByID = `-- name: DeleteNarInfoByID :exec +DELETE FROM narinfos +WHERE id = ? +` + +func (q *Queries) DeleteNarInfoByID(ctx context.Context, id int64) error { + _, err := q.db.ExecContext(ctx, deleteNarInfoByID, id) + return err +} + +const getLeastUsedNars = `-- name: GetLeastUsedNars :many +SELECT + n1.id, n1.narinfo_id, n1.hash, n1.compression, n1.file_size, n1.created_at, n1.updated_at, n1.last_accessed_at +FROM nars n1 +WHERE ( + SELECT SUM(n2.file_size) + FROM nars n2 + WHERE n2.last_accessed_at <= n1.last_accessed_at +) <= ? +` + +func (q *Queries) GetLeastUsedNars(ctx context.Context, fileSize int64) ([]Nar, error) { + rows, err := q.db.QueryContext(ctx, getLeastUsedNars, fileSize) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Nar + for rows.Next() { + var i Nar + if err := rows.Scan( + &i.ID, + &i.NarInfoID, + &i.Hash, + &i.Compression, + &i.FileSize, + &i.CreatedAt, + &i.UpdatedAt, + &i.LastAccessedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getNarByHash = `-- name: GetNarByHash :one +SELECT id, narinfo_id, hash, compression, file_size, created_at, updated_at, last_accessed_at +FROM nars +WHERE hash = ? +` + +func (q *Queries) GetNarByHash(ctx context.Context, hash string) (Nar, error) { + row := q.db.QueryRowContext(ctx, getNarByHash, hash) + var i Nar + err := row.Scan( + &i.ID, + &i.NarInfoID, + &i.Hash, + &i.Compression, + &i.FileSize, + &i.CreatedAt, + &i.UpdatedAt, + &i.LastAccessedAt, + ) + return i, err +} + +const getNarByID = `-- name: GetNarByID :one +SELECT id, narinfo_id, hash, compression, file_size, created_at, updated_at, last_accessed_at +FROM nars +WHERE id = ? +` + +func (q *Queries) GetNarByID(ctx context.Context, id int64) (Nar, error) { + row := q.db.QueryRowContext(ctx, getNarByID, id) + var i Nar + err := row.Scan( + &i.ID, + &i.NarInfoID, + &i.Hash, + &i.Compression, + &i.FileSize, + &i.CreatedAt, + &i.UpdatedAt, + &i.LastAccessedAt, + ) + return i, err +} + +const getNarInfoByHash = `-- name: GetNarInfoByHash :one +SELECT id, hash, created_at, updated_at, last_accessed_at +FROM narinfos +WHERE hash = ? +` + +func (q *Queries) GetNarInfoByHash(ctx context.Context, hash string) (NarInfo, error) { + row := q.db.QueryRowContext(ctx, getNarInfoByHash, hash) + var i NarInfo + err := row.Scan( + &i.ID, + &i.Hash, + &i.CreatedAt, + &i.UpdatedAt, + &i.LastAccessedAt, + ) + return i, err +} + +const getNarInfoByID = `-- name: GetNarInfoByID :one +SELECT id, hash, created_at, updated_at, last_accessed_at +FROM narinfos +WHERE id = ? +` + +func (q *Queries) GetNarInfoByID(ctx context.Context, id int64) (NarInfo, error) { + row := q.db.QueryRowContext(ctx, getNarInfoByID, id) + var i NarInfo + err := row.Scan( + &i.ID, + &i.Hash, + &i.CreatedAt, + &i.UpdatedAt, + &i.LastAccessedAt, + ) + return i, err +} + +const getNarTotalSize = `-- name: GetNarTotalSize :one +SELECT SUM(file_size) AS total_size +FROM nars +` + +func (q *Queries) GetNarTotalSize(ctx context.Context) (sql.NullFloat64, error) { + row := q.db.QueryRowContext(ctx, getNarTotalSize) + var total_size sql.NullFloat64 + err := row.Scan(&total_size) + return total_size, err +} + +const touchNar = `-- name: TouchNar :exec +UPDATE nars +SET last_accessed_at = CURRENT_TIMESTAMP, + updated_at = CURRENT_TIMESTAMP +WHERE hash = ? +` + +func (q *Queries) TouchNar(ctx context.Context, hash string) error { + _, err := q.db.ExecContext(ctx, touchNar, hash) + return err +} + +const touchNarInfo = `-- name: TouchNarInfo :exec +UPDATE narinfos +SET last_accessed_at = CURRENT_TIMESTAMP, + updated_at = CURRENT_TIMESTAMP +WHERE hash = ? +` + +func (q *Queries) TouchNarInfo(ctx context.Context, hash string) error { + _, err := q.db.ExecContext(ctx, touchNarInfo, hash) + return err +} diff --git a/pkg/database/sqlite.go b/pkg/database/sqlite.go index bdc463f..437fe4d 100644 --- a/pkg/database/sqlite.go +++ b/pkg/database/sqlite.go @@ -4,7 +4,6 @@ import ( "database/sql" "errors" "fmt" - "time" "github.com/inconshreveable/log15/v3" "github.com/mattn/go-sqlite3" @@ -153,30 +152,6 @@ type ( logger log15.Logger } - - // NarInfoModel represents a narinfo record in the database; This is not the - // same as narinfo.NarInfo! - NarInfoModel struct { - ID int64 - Hash string - - CreatedAt time.Time - UpdatedAt *time.Time - LastAccessedAt time.Time - } - - // NarModel represents a nar record in the database. - NarModel struct { - ID int64 - NarInfoID int64 - Hash string - Compression string - FileSize uint64 - - CreatedAt time.Time - UpdatedAt *time.Time - LastAccessedAt time.Time - } ) // Open opens a sqlite3 database, and creates it if necessary. @@ -198,13 +173,13 @@ func Open(logger log15.Logger, dbpath string) (*DB, error) { // GetNarInfoRecordByID returns a narinfo record given its hash. If no nar was // found with the given hash then ErrNotFound is returned instead. -func (db *DB) GetNarInfoRecordByID(tx *sql.Tx, id int64) (NarInfoModel, error) { +func (db *DB) GetNarInfoRecordByID(tx *sql.Tx, id int64) (NarInfo, error) { return db.getNarInfoRecord(tx, getNarInfoIDQuery, id) } // GetNarInfoRecord returns a narinfo record given its hash. If no nar was // found with the given hash then ErrNotFound is returned instead. -func (db *DB) GetNarInfoRecord(tx *sql.Tx, hash string) (NarInfoModel, error) { +func (db *DB) GetNarInfoRecord(tx *sql.Tx, hash string) (NarInfo, error) { return db.getNarInfoRecord(tx, getNarInfoQuery, hash) } @@ -244,8 +219,8 @@ func (db *DB) DeleteNarInfoRecord(tx *sql.Tx, hash string) error { // GetNarRecord returns a nar record given its hash. If no nar was found with // the given hash then ErrNotFound is returned instead. -func (db *DB) GetNarRecord(tx *sql.Tx, hash string) (NarModel, error) { - var nm NarModel +func (db *DB) GetNarRecord(tx *sql.Tx, hash string) (Nar, error) { + var nm Nar stmt, err := tx.Prepare(getNarQuery) if err != nil { @@ -259,7 +234,7 @@ func (db *DB) GetNarRecord(tx *sql.Tx, hash string) (NarModel, error) { } defer rows.Close() - nms := make([]NarModel, 0) + nms := make([]Nar, 0) for rows.Next() { err := rows.Scan( @@ -273,7 +248,7 @@ func (db *DB) GetNarRecord(tx *sql.Tx, hash string) (NarModel, error) { &nm.LastAccessedAt, ) if err != nil { - return nm, fmt.Errorf("error scanning the row into a NarModel: %w", err) + return nm, fmt.Errorf("error scanning the row into a Nar: %w", err) } nms = append(nms, nm) @@ -357,7 +332,7 @@ func (db *DB) NarTotalSize(tx *sql.Tx) (uint64, error) { // GetLeastAccessedNarRecords returns all records with the oldest // last_accessed_at up to totalFileSize left behind. -func (db *DB) GetLeastAccessedNarRecords(tx *sql.Tx, totalFileSize uint64) ([]NarModel, error) { +func (db *DB) GetLeastAccessedNarRecords(tx *sql.Tx, totalFileSize uint64) ([]Nar, error) { stmt, err := tx.Prepare(leastUsedNarsQuery) if err != nil { return nil, fmt.Errorf("error preparing a statement: %w", err) @@ -370,10 +345,10 @@ func (db *DB) GetLeastAccessedNarRecords(tx *sql.Tx, totalFileSize uint64) ([]Na } defer rows.Close() - nms := make([]NarModel, 0) + nms := make([]Nar, 0) for rows.Next() { - var nm NarModel + var nm Nar err := rows.Scan( &nm.ID, @@ -386,7 +361,7 @@ func (db *DB) GetLeastAccessedNarRecords(tx *sql.Tx, totalFileSize uint64) ([]Na &nm.LastAccessedAt, ) if err != nil { - return nms, fmt.Errorf("error scanning the row into a NarModel: %w", err) + return nms, fmt.Errorf("error scanning the row into a Nar: %w", err) } nms = append(nms, nm) @@ -430,8 +405,8 @@ func (db *DB) createTables() error { return nil } -func (db *DB) getNarInfoRecord(tx *sql.Tx, query string, args ...any) (NarInfoModel, error) { - var nim NarInfoModel +func (db *DB) getNarInfoRecord(tx *sql.Tx, query string, args ...any) (NarInfo, error) { + var nim NarInfo stmt, err := tx.Prepare(query) if err != nil { @@ -445,11 +420,11 @@ func (db *DB) getNarInfoRecord(tx *sql.Tx, query string, args ...any) (NarInfoMo } defer rows.Close() - nims := make([]NarInfoModel, 0) + nims := make([]NarInfo, 0) for rows.Next() { if err := rows.Scan(&nim.ID, &nim.Hash, &nim.CreatedAt, &nim.UpdatedAt, &nim.LastAccessedAt); err != nil { - return nim, fmt.Errorf("error scanning the row into a NarInfoModel: %w", err) + return nim, fmt.Errorf("error scanning the row into a NarInfo: %w", err) } nims = append(nims, nim) diff --git a/pkg/database/sqlite_test.go b/pkg/database/sqlite_test.go index f29973e..cb4f200 100644 --- a/pkg/database/sqlite_test.go +++ b/pkg/database/sqlite_test.go @@ -123,10 +123,10 @@ func TestInsertNarInfoRecord(t *testing.T) { defer rows.Close() - nims := make([]database.NarInfoModel, 0) + nims := make([]database.NarInfo, 0) for rows.Next() { - var nim database.NarInfoModel + var nim database.NarInfo err := rows.Scan(&nim.ID, &nim.Hash, &nim.CreatedAt, &nim.UpdatedAt, &nim.LastAccessedAt) require.NoError(t, err) @@ -143,8 +143,8 @@ func TestInsertNarInfoRecord(t *testing.T) { assert.Equal(t, lid, nims[0].ID) assert.Equal(t, hash, nims[0].Hash) assert.Less(t, time.Since(nims[0].CreatedAt), 3*time.Second) - assert.Nil(t, nims[0].UpdatedAt) - assert.Equal(t, nims[0].CreatedAt, nims[0].LastAccessedAt) + assert.False(t, nims[0].UpdatedAt.Valid) + assert.Equal(t, nims[0].CreatedAt, nims[0].LastAccessedAt.Time) } }) @@ -287,10 +287,10 @@ func TestTouchNarInfoRecord(t *testing.T) { defer rows.Close() - nims := make([]database.NarInfoModel, 0) + nims := make([]database.NarInfo, 0) for rows.Next() { - var nim database.NarInfoModel + var nim database.NarInfo err := rows.Scan(&nim.ID, &nim.Hash, &nim.CreatedAt, &nim.UpdatedAt, &nim.LastAccessedAt) require.NoError(t, err) @@ -301,8 +301,8 @@ func TestTouchNarInfoRecord(t *testing.T) { require.NoError(t, rows.Err()) assert.Len(t, nims, 1) - assert.Equal(t, nims[0].CreatedAt, nims[0].LastAccessedAt) - assert.Nil(t, nims[0].UpdatedAt) + assert.Equal(t, nims[0].CreatedAt, nims[0].LastAccessedAt.Time) + assert.False(t, nims[0].UpdatedAt.Valid) }) t.Run("touch the narinfo", func(t *testing.T) { @@ -331,10 +331,10 @@ func TestTouchNarInfoRecord(t *testing.T) { defer rows.Close() - nims := make([]database.NarInfoModel, 0) + nims := make([]database.NarInfo, 0) for rows.Next() { - var nim database.NarInfoModel + var nim database.NarInfo err := rows.Scan(&nim.ID, &nim.Hash, &nim.CreatedAt, &nim.UpdatedAt, &nim.LastAccessedAt) require.NoError(t, err) @@ -346,7 +346,10 @@ func TestTouchNarInfoRecord(t *testing.T) { assert.Len(t, nims, 1) assert.NotEqual(t, nims[0].CreatedAt, nims[0].LastAccessedAt) - assert.Equal(t, *nims[0].UpdatedAt, nims[0].LastAccessedAt) + + if assert.True(t, nims[0].UpdatedAt.Valid) { + assert.Equal(t, nims[0].UpdatedAt.Time, nims[0].LastAccessedAt.Time) + } }) }) } @@ -414,10 +417,10 @@ func TestDeleteNarInfoRecord(t *testing.T) { defer rows.Close() - nims := make([]database.NarInfoModel, 0) + nims := make([]database.NarInfo, 0) for rows.Next() { - var nim database.NarInfoModel + var nim database.NarInfo err := rows.Scan(&nim.ID, &nim.Hash, &nim.CreatedAt, &nim.UpdatedAt, &nim.LastAccessedAt) require.NoError(t, err) @@ -490,10 +493,10 @@ func TestInsertNarRecord(t *testing.T) { defer rows.Close() - nims := make([]database.NarModel, 0) + nims := make([]database.Nar, 0) for rows.Next() { - var nim database.NarModel + var nim database.Nar err := rows.Scan( &nim.ID, @@ -522,8 +525,8 @@ func TestInsertNarRecord(t *testing.T) { assert.Equal(t, compression, nims[0].Compression) assert.EqualValues(t, 123, nims[0].FileSize) assert.Less(t, time.Since(nims[0].CreatedAt), 3*time.Second) - assert.Nil(t, nims[0].UpdatedAt) - assert.Equal(t, nims[0].CreatedAt, nims[0].LastAccessedAt) + assert.False(t, nims[0].UpdatedAt.Valid) + assert.Equal(t, nims[0].CreatedAt, nims[0].LastAccessedAt.Time) } }) @@ -636,10 +639,10 @@ func TestTouchNarRecord(t *testing.T) { defer rows.Close() - nims := make([]database.NarModel, 0) + nims := make([]database.Nar, 0) for rows.Next() { - var nim database.NarModel + var nim database.Nar err := rows.Scan( &nim.ID, @@ -659,8 +662,8 @@ func TestTouchNarRecord(t *testing.T) { require.NoError(t, rows.Err()) if assert.Len(t, nims, 1) { - assert.Nil(t, nims[0].UpdatedAt) - assert.Equal(t, nims[0].CreatedAt, nims[0].LastAccessedAt) + assert.False(t, nims[0].UpdatedAt.Valid) + assert.Equal(t, nims[0].CreatedAt, nims[0].LastAccessedAt.Time) } }) @@ -695,10 +698,10 @@ func TestTouchNarRecord(t *testing.T) { defer rows.Close() - nims := make([]database.NarModel, 0) + nims := make([]database.Nar, 0) for rows.Next() { - var nim database.NarModel + var nim database.Nar err := rows.Scan( &nim.ID, @@ -719,7 +722,10 @@ func TestTouchNarRecord(t *testing.T) { if assert.Len(t, nims, 1) { assert.NotEqual(t, nims[0].CreatedAt, nims[0].LastAccessedAt) - assert.Equal(t, *nims[0].UpdatedAt, nims[0].LastAccessedAt) + + if assert.True(t, nims[0].UpdatedAt.Valid) { + assert.Equal(t, nims[0].UpdatedAt.Time, nims[0].LastAccessedAt.Time) + } } }) }) @@ -815,10 +821,10 @@ func TestDeleteNarRecord(t *testing.T) { defer rows.Close() - nims := make([]database.NarModel, 0) + nims := make([]database.Nar, 0) for rows.Next() { - var nim database.NarModel + var nim database.Nar err := rows.Scan( &nim.ID, diff --git a/sqlc.yml b/sqlc.yml new file mode 100644 index 0000000..81c7f81 --- /dev/null +++ b/sqlc.yml @@ -0,0 +1,15 @@ +version: "2" +sql: + - engine: "sqlite" + queries: "db/query.sql" + schema: "db/migrations" + gen: + go: + package: "database" + out: "pkg/database" + rename: + narinfo_id: NarInfoID +overrides: + go: + rename: + narinfo: NarInfo