Use sqlc to generate typesafe repository tools

master
Caj Larsson 3 years ago
parent 1c15742710
commit d688900bae

@ -11,7 +11,9 @@ type Namespace struct {
LastSeen time.Time
AllowanceDuration time.Duration
FileQuota FileSizeQuota
Usage Usage
Usage FileStat
Download FileStat
Upload FileStat
}
var (

@ -25,7 +25,9 @@ func basicNamespaceContract(fac func() Repository, t *testing.T) {
time.Now(),
time.Duration(time.Hour * 3),
FileSizeQuota{1000, 0},
Usage{1, 2, 3, 4, 5},
FileStat{1, 2},
FileStat{3, 4},
FileStat{5, 6},
}
ns1, _ := r.Create(ns)
@ -42,7 +44,7 @@ func basicNamespaceContract(fac func() Repository, t *testing.T) {
ns3, _ := r.GetByName("n2")
is.Equal(ns3, ns2)
is.Equal(*ns3, *ns2)
is.NoErr(r.Delete(ns2.ID))

@ -33,30 +33,14 @@ func (f *FileSizeQuota) Remove(size int64) error {
return nil
}
type Usage struct {
Stored int64
Uploads int64
UploadB int64
Downloads int64
DownloadB int64
type FileStat struct {
Num int64
SizeB int64
}
func (u Usage) Uploaded(size int64) Usage {
return Usage{
u.Stored,
u.Uploads + 1,
u.UploadB + size,
u.Downloads,
u.DownloadB,
}
}
func (u Usage) Downloaded(size int64) Usage {
return Usage{
u.Stored,
u.Uploads,
u.UploadB,
u.Downloads + 1,
u.DownloadB + size,
func (s FileStat) Add(size int64) FileStat {
return FileStat{
s.Num + 1,
s.SizeB + size,
}
}

@ -0,0 +1,31 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.13.0
package namespace
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,
}
}

@ -0,0 +1,27 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.13.0
package namespace
import (
"database/sql"
)
type FileStat struct {
ID int64
Num int64
SizeB int64
}
type Namespace struct {
ID int64
Name string
Lastseen int64
AllowanceTime sql.NullInt64
QuotaKb sql.NullInt64
QuotaUsageKb sql.NullInt64
UsageID int64
DownloadID int64
UploadID int64
}

@ -0,0 +1,84 @@
-- name: CreateNamespace :one
INSERT INTO
namespace(
name,
lastseen,
allowance_time,
quota_kb,
quota_usage_kb,
usage_id,
download_id,
upload_id
)
values(?, ?, ?, ?, ?, ?, ?, ?)
returning id;
-- name: CreateFileStats :one
INSERT INTO file_stats(num, size_b)
values(?, ?)
returning id;
-- name: AllNamespaces :many
SELECT
ns.id,
ns.name,
ns.lastseen,
ns.allowance_time,
ns.quota_kb,
ns.quota_usage_kb,
u.num as u_num,
u.size_b as u_size_b,
d.num as d_num,
d.size_b as d_size_b,
ul.num as ul_num,
ul.size_b as ul_size_b
FROM namespace as ns
JOIN file_stats as u
ON ns.usage_id = u.id
JOIN file_stats as d
ON ns.download_id = d.id
JOIN file_stats as ul
ON ns.upload_id = ul.id;
-- name: GetNamespaceByName :one
SELECT
ns.id,
ns.name,
ns.lastseen,
ns.allowance_time,
ns.quota_kb,
ns.quota_usage_kb,
u.num as u_num,
u.size_b as u_size_b,
d.num as d_num,
d.size_b as d_size_b,
ul.num as ul_num,
ul.size_b as ul_size_b
FROM namespace as ns
JOIN file_stats as u
ON ns.usage_id = u.id
JOIN file_stats as d
ON ns.download_id = d.id
JOIN file_stats as ul
ON ns.upload_id = ul.id
WHERE ns.name = ?;
-- name: GetFileStat :one
SELECT * FROM file_stats where id = ?;
-- name: UpdateFileStat :exec
UPDATE file_stats SET num = ?, size_b = ? where id = ?;
-- name: UpdateNamespace :one
UPDATE namespace SET
name = ?,
lastseen = ?,
allowance_time = ?,
quota_kb = ?,
quota_usage_kb = ?
WHERE id = ?
RETURNING usage_id, download_id, upload_id;
-- name: DeleteNameSpace :exec
DELETE FROM namespace where id = ?;

@ -0,0 +1,280 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.13.0
// source: queries.sql
package namespace
import (
"context"
"database/sql"
)
const allNamespaces = `-- name: AllNamespaces :many
SELECT
ns.id,
ns.name,
ns.lastseen,
ns.allowance_time,
ns.quota_kb,
ns.quota_usage_kb,
u.num as u_num,
u.size_b as u_size_b,
d.num as d_num,
d.size_b as d_size_b,
ul.num as ul_num,
ul.size_b as ul_size_b
FROM namespace as ns
JOIN file_stats as u
ON ns.usage_id = u.id
JOIN file_stats as d
ON ns.download_id = d.id
JOIN file_stats as ul
ON ns.upload_id = ul.id
`
type AllNamespacesRow struct {
ID int64
Name string
Lastseen int64
AllowanceTime sql.NullInt64
QuotaKb sql.NullInt64
QuotaUsageKb sql.NullInt64
UNum int64
USizeB int64
DNum int64
DSizeB int64
UlNum int64
UlSizeB int64
}
func (q *Queries) AllNamespaces(ctx context.Context) ([]AllNamespacesRow, error) {
rows, err := q.db.QueryContext(ctx, allNamespaces)
if err != nil {
return nil, err
}
defer rows.Close()
var items []AllNamespacesRow
for rows.Next() {
var i AllNamespacesRow
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Lastseen,
&i.AllowanceTime,
&i.QuotaKb,
&i.QuotaUsageKb,
&i.UNum,
&i.USizeB,
&i.DNum,
&i.DSizeB,
&i.UlNum,
&i.UlSizeB,
); 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 createFileStats = `-- name: CreateFileStats :one
INSERT INTO file_stats(num, size_b)
values(?, ?)
returning id
`
type CreateFileStatsParams struct {
Num int64
SizeB int64
}
func (q *Queries) CreateFileStats(ctx context.Context, arg CreateFileStatsParams) (int64, error) {
row := q.db.QueryRowContext(ctx, createFileStats, arg.Num, arg.SizeB)
var id int64
err := row.Scan(&id)
return id, err
}
const createNamespace = `-- name: CreateNamespace :one
INSERT INTO
namespace(
name,
lastseen,
allowance_time,
quota_kb,
quota_usage_kb,
usage_id,
download_id,
upload_id
)
values(?, ?, ?, ?, ?, ?, ?, ?)
returning id
`
type CreateNamespaceParams struct {
Name string
Lastseen int64
AllowanceTime sql.NullInt64
QuotaKb sql.NullInt64
QuotaUsageKb sql.NullInt64
UsageID int64
DownloadID int64
UploadID int64
}
func (q *Queries) CreateNamespace(ctx context.Context, arg CreateNamespaceParams) (int64, error) {
row := q.db.QueryRowContext(ctx, createNamespace,
arg.Name,
arg.Lastseen,
arg.AllowanceTime,
arg.QuotaKb,
arg.QuotaUsageKb,
arg.UsageID,
arg.DownloadID,
arg.UploadID,
)
var id int64
err := row.Scan(&id)
return id, err
}
const deleteNameSpace = `-- name: DeleteNameSpace :exec
DELETE FROM namespace where id = ?
`
func (q *Queries) DeleteNameSpace(ctx context.Context, id int64) error {
_, err := q.db.ExecContext(ctx, deleteNameSpace, id)
return err
}
const getFileStat = `-- name: GetFileStat :one
SELECT id, num, size_b FROM file_stats where id = ?
`
func (q *Queries) GetFileStat(ctx context.Context, id int64) (FileStat, error) {
row := q.db.QueryRowContext(ctx, getFileStat, id)
var i FileStat
err := row.Scan(&i.ID, &i.Num, &i.SizeB)
return i, err
}
const getNamespaceByName = `-- name: GetNamespaceByName :one
SELECT
ns.id,
ns.name,
ns.lastseen,
ns.allowance_time,
ns.quota_kb,
ns.quota_usage_kb,
u.num as u_num,
u.size_b as u_size_b,
d.num as d_num,
d.size_b as d_size_b,
ul.num as ul_num,
ul.size_b as ul_size_b
FROM namespace as ns
JOIN file_stats as u
ON ns.usage_id = u.id
JOIN file_stats as d
ON ns.download_id = d.id
JOIN file_stats as ul
ON ns.upload_id = ul.id
WHERE ns.name = ?
`
type GetNamespaceByNameRow struct {
ID int64
Name string
Lastseen int64
AllowanceTime sql.NullInt64
QuotaKb sql.NullInt64
QuotaUsageKb sql.NullInt64
UNum int64
USizeB int64
DNum int64
DSizeB int64
UlNum int64
UlSizeB int64
}
func (q *Queries) GetNamespaceByName(ctx context.Context, name string) (GetNamespaceByNameRow, error) {
row := q.db.QueryRowContext(ctx, getNamespaceByName, name)
var i GetNamespaceByNameRow
err := row.Scan(
&i.ID,
&i.Name,
&i.Lastseen,
&i.AllowanceTime,
&i.QuotaKb,
&i.QuotaUsageKb,
&i.UNum,
&i.USizeB,
&i.DNum,
&i.DSizeB,
&i.UlNum,
&i.UlSizeB,
)
return i, err
}
const updateFileStat = `-- name: UpdateFileStat :exec
UPDATE file_stats SET num = ?, size_b = ? where id = ?
`
type UpdateFileStatParams struct {
Num int64
SizeB int64
ID int64
}
func (q *Queries) UpdateFileStat(ctx context.Context, arg UpdateFileStatParams) error {
_, err := q.db.ExecContext(ctx, updateFileStat, arg.Num, arg.SizeB, arg.ID)
return err
}
const updateNamespace = `-- name: UpdateNamespace :one
UPDATE namespace SET
name = ?,
lastseen = ?,
allowance_time = ?,
quota_kb = ?,
quota_usage_kb = ?
WHERE id = ?
RETURNING usage_id, download_id, upload_id
`
type UpdateNamespaceParams struct {
Name string
Lastseen int64
AllowanceTime sql.NullInt64
QuotaKb sql.NullInt64
QuotaUsageKb sql.NullInt64
ID int64
}
type UpdateNamespaceRow struct {
UsageID int64
DownloadID int64
UploadID int64
}
func (q *Queries) UpdateNamespace(ctx context.Context, arg UpdateNamespaceParams) (UpdateNamespaceRow, error) {
row := q.db.QueryRowContext(ctx, updateNamespace,
arg.Name,
arg.Lastseen,
arg.AllowanceTime,
arg.QuotaKb,
arg.QuotaUsageKb,
arg.ID,
)
var i UpdateNamespaceRow
err := row.Scan(&i.UsageID, &i.DownloadID, &i.UploadID)
return i, err
}

@ -2,153 +2,203 @@ package namespace
import (
"caj-larsson/bog/dataswamp/namespace"
"context"
"database/sql"
"errors"
"github.com/mattn/go-sqlite3"
"time"
)
var _ = sqlite3.ErrError
type Repository struct {
db *sql.DB
}
func (r *Repository) migrate() error {
query := `
CREATE TABLE IF NOT EXISTS file_stats(
id INTEGER PRIMARY KEY,
num BIGINT NOT NULL,
size_b BIGINT NOT NULL
);`
_, err := r.db.Exec(query)
if err != nil {
return err
}
query = `
CREATE TABLE IF NOT EXISTS namespace(
id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
lastseen TEXT,
allowance_time BIGINT,
quota_kb BIGINT,
quota_usage_kb BIGINT,
usage_id BIGINT NOT NULL REFERENCES file_stats(Id),
download_id BIGINT NOT NULL REFERENCES file_stats(Id),
upload_id BIGINT NOT NULL REFERENCES file_stats(Id)
);`
_, err = r.db.Exec(query)
return err
}
func NewRepository(filename string) namespace.Repository {
db, err := sql.Open("sqlite3", filename)
if err != nil {
panic(err)
}
repo := Repository{
db: db,
}
repo.migrate()
return &repo
}
func (r Repository) migrate() error {
query := `
CREATE TABLE IF NOT EXISTS namespace(
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
lastseen text,
allowance_time bigint,
quota_kb bigint,
quota_usage_kb bigint
);
`
_, err := r.db.Exec(query)
return err
func (q *Queries) createFileStats(ctx context.Context, fstat namespace.FileStat) (int64, error) {
return q.CreateFileStats(ctx, CreateFileStatsParams{fstat.Num, fstat.SizeB})
}
func (r *Repository) Create(ns namespace.Namespace) (*namespace.Namespace, error) {
var record, err = fromEntity(ns)
ctx := context.Background()
q := New(r.db)
u_id, err := q.createFileStats(ctx, ns.Usage)
if err != nil {
return nil, err
}
dl_id, err := q.createFileStats(ctx, ns.Download)
if err != nil {
return nil, err
}
res, err := r.db.Exec(
"INSERT INTO namespace(name, lastseen, allowance_time, quota_kb, quota_usage_kb) values(?,?,?,?,?)",
record.Name, record.LastSeen, record.AllowanceSeconds, record.QuotaKB, record.QuotaUsedKB,
)
ul_id, err := q.createFileStats(ctx, ns.Upload)
if err != nil {
var sqliteErr sqlite3.Error
if errors.As(err, &sqliteErr) {
if errors.Is(sqliteErr.ExtendedCode, sqlite3.ErrConstraintUnique) {
return nil, namespace.ErrDuplicate
}
}
return nil, err
}
id, err := res.LastInsertId()
ns.LastSeen = ns.LastSeen.Round(time.Microsecond)
p := CreateNamespaceParams{
Name: ns.Name,
Lastseen: ns.LastSeen.UnixMicro(),
AllowanceTime: sql.NullInt64{int64(ns.AllowanceDuration.Seconds()), true},
QuotaKb: sql.NullInt64{int64(ns.FileQuota.AllowanceKB), true},
QuotaUsageKb: sql.NullInt64{int64(ns.FileQuota.CurrentUsage), true},
UsageID: u_id,
DownloadID: dl_id,
UploadID: ul_id,
}
id, err := q.CreateNamespace(ctx, p)
if err != nil {
return nil, err
}
ns.ID = id
ns.ID = id
return &ns, nil
}
func (r Repository) All() ([]namespace.Namespace, error) {
rows, err := r.db.Query("SELECT * FROM namespace")
func (r *Repository) All() ([]namespace.Namespace, error) {
ctx := context.Background()
q := New(r.db)
rows, err := q.AllNamespaces(ctx)
if err != nil {
return nil, err
}
defer rows.Close()
var all []namespace.Namespace
for rows.Next() {
var record NamespaceRecord
if err := rows.Scan(&record.ID, &record.Name, &record.LastSeen, &record.AllowanceSeconds, &record.QuotaKB, &record.QuotaUsedKB); err != nil {
return nil, err
for _, row := range rows {
ns := namespace.Namespace{
row.ID,
row.Name,
time.UnixMicro(row.Lastseen),
time.Duration(row.AllowanceTime.Int64 * int64(time.Second)),
namespace.FileSizeQuota{row.QuotaKb.Int64, row.QuotaUsageKb.Int64},
namespace.FileStat{row.UNum, row.USizeB},
namespace.FileStat{row.DNum, row.DSizeB},
namespace.FileStat{row.UlNum, row.UlSizeB},
}
var ns, err = record.toEntity()
if err != nil {
return nil, err
}
all = append(all, *ns)
all = append(all, ns)
}
return all, nil
}
func (r Repository) GetByName(name string) (*namespace.Namespace, error) {
row := r.db.QueryRow("SELECT id, name, lastseen, allowance_time, quota_kb, quota_usage_kb FROM namespace WHERE name = ?", name)
func (r *Repository) GetByName(name string) (*namespace.Namespace, error) {
ctx := context.Background()
q := New(r.db)
row, err := q.GetNamespaceByName(ctx, name)
var record NamespaceRecord
if err := row.Scan(&record.ID, &record.Name, &record.LastSeen, &record.AllowanceSeconds, &record.QuotaKB, &record.QuotaUsedKB); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, namespace.ErrNotExists
}
if err != nil {
return nil, err
}
var ns, err = record.toEntity()
if err != nil {
return nil, err
ns := namespace.Namespace{
row.ID,
row.Name,
time.UnixMicro(row.Lastseen),
time.Duration(row.AllowanceTime.Int64 * int64(time.Second)),
namespace.FileSizeQuota{row.QuotaKb.Int64, row.QuotaUsageKb.Int64},
namespace.FileStat{row.UNum, row.USizeB},
namespace.FileStat{row.DNum, row.DSizeB},
namespace.FileStat{row.UlNum, row.UlSizeB},
}
return ns, nil
return &ns, nil
}
func (r Repository) Update(id int64, updated namespace.Namespace) (*namespace.Namespace, error) {
if id == 0 {
return nil, errors.New("invalid updated ID")
}
func (q *Queries) updateFileStat(ctx context.Context, id int64, fstat namespace.FileStat) error {
return q.UpdateFileStat(ctx, UpdateFileStatParams{fstat.Num, fstat.SizeB, id})
}
var record, err = fromEntity(updated)
func (r *Repository) Update(id int64, ns namespace.Namespace) (*namespace.Namespace, error) {
ctx := context.Background()
q := New(r.db)
res, err := r.db.Exec("UPDATE namespace SET name = ?, lastseen = ?, allowance_time = ?, quota_kb = ?, quota_usage_kb = ? WHERE id = ?",
record.Name, record.LastSeen, record.AllowanceSeconds, &record.QuotaKB, &record.QuotaUsedKB, id)
ids, err := q.UpdateNamespace(ctx, UpdateNamespaceParams{
ns.Name,
ns.LastSeen.Round(time.Microsecond).UnixMicro(),
sql.NullInt64{int64(ns.AllowanceDuration.Seconds()), true},
sql.NullInt64{int64(ns.FileQuota.AllowanceKB), true},
sql.NullInt64{int64(ns.FileQuota.CurrentUsage), true},
ns.ID,
})
err = q.updateFileStat(ctx, ids.UsageID, ns.Usage)
if err != nil {
return nil, err
}
rowsAffected, err := res.RowsAffected()
err = q.updateFileStat(ctx, ids.DownloadID, ns.Download)
if err != nil {
return nil, err
}
if rowsAffected == 0 {
return nil, namespace.ErrUpdateFailed
err = q.updateFileStat(ctx, ids.UploadID, ns.Upload)
if err != nil {
return nil, err
}
return &updated, nil
return &ns, nil
}
func (r Repository) Delete(id int64) error {
res, err := r.db.Exec("DELETE FROM namespace WHERE id = ?", id)
if err != nil {
return err
}
func (r *Repository) Delete(id int64) error {
ctx := context.Background()
q := New(r.db)
rowsAffected, err := res.RowsAffected()
err := q.DeleteNameSpace(ctx, id)
if err != nil {
return err
}
if rowsAffected == 0 {
return namespace.ErrDeleteFailed
}
return err
return nil
}

@ -2,15 +2,9 @@ package namespace
import (
"caj-larsson/bog/dataswamp/namespace"
"path"
"testing"
)
func TestFileRepo(t *testing.T) {
fac := func() namespace.Repository {
d := t.TempDir()
db_path := path.Join(d, "db.sql")
return NewRepository(db_path)
}
namespace.RepositoryContract(fac, t)
namespace.RepositoryContract(func() namespace.Repository { return NewRepository(":memory:") }, t)
}

@ -0,0 +1,17 @@
CREATE TABLE IF NOT EXISTS file_stats(
id BIGINT PRIMARY KEY,
num BIGINT NOT NULL,
size_b BIGINT NOT NULL
);
CREATE TABLE IF NOT EXISTS namespace(
id BIGINT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
lastseen BIGINT NOT NULL,
allowance_time BIGINT,
quota_kb BIGINT,
quota_usage_kb BIGINT,
usage_id BIGINT NOT NULL REFERENCES file_stats(Id) ON DELETE CASCADE,
download_id BIGINT NOT NULL REFERENCES file_stats(Id) ON DELETE CASCADE,
upload_id BIGINT NOT NULL REFERENCES file_stats(Id) ON DELETE CASCADE
);

@ -0,0 +1,8 @@
version: "1"
packages:
- path: infrastructure/sqlite/namespace
name: namespace
engine: postgresql
schema: infrastructure/sqlite/namespace/schema.sql
queries: infrastructure/sqlite/namespace/queries.sql
Loading…
Cancel
Save