diff --git a/dataswamp/namespace/entities.go b/dataswamp/namespace/entities.go index 28a32ff..b46e24a 100644 --- a/dataswamp/namespace/entities.go +++ b/dataswamp/namespace/entities.go @@ -11,7 +11,9 @@ type Namespace struct { LastSeen time.Time AllowanceDuration time.Duration FileQuota FileSizeQuota - Usage Usage + Usage FileStat + Download FileStat + Upload FileStat } var ( diff --git a/dataswamp/namespace/repository_contract.go b/dataswamp/namespace/repository_contract.go index dbb7151..728ab2c 100644 --- a/dataswamp/namespace/repository_contract.go +++ b/dataswamp/namespace/repository_contract.go @@ -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)) diff --git a/dataswamp/namespace/valueobjects.go b/dataswamp/namespace/valueobjects.go index 2675c36..4dd6ea6 100644 --- a/dataswamp/namespace/valueobjects.go +++ b/dataswamp/namespace/valueobjects.go @@ -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, } } diff --git a/infrastructure/sqlite/namespace/db.go b/infrastructure/sqlite/namespace/db.go new file mode 100644 index 0000000..7352e95 --- /dev/null +++ b/infrastructure/sqlite/namespace/db.go @@ -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, + } +} diff --git a/infrastructure/sqlite/namespace/models.go b/infrastructure/sqlite/namespace/models.go new file mode 100644 index 0000000..9625940 --- /dev/null +++ b/infrastructure/sqlite/namespace/models.go @@ -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 +} diff --git a/infrastructure/sqlite/namespace/queries.sql b/infrastructure/sqlite/namespace/queries.sql new file mode 100644 index 0000000..5da0a80 --- /dev/null +++ b/infrastructure/sqlite/namespace/queries.sql @@ -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 = ?; diff --git a/infrastructure/sqlite/namespace/queries.sql.go b/infrastructure/sqlite/namespace/queries.sql.go new file mode 100644 index 0000000..7851f8c --- /dev/null +++ b/infrastructure/sqlite/namespace/queries.sql.go @@ -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 +} diff --git a/infrastructure/sqlite/namespace/repository.go b/infrastructure/sqlite/namespace/repository.go index 191e985..84af638 100644 --- a/infrastructure/sqlite/namespace/repository.go +++ b/infrastructure/sqlite/namespace/repository.go @@ -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 } diff --git a/infrastructure/sqlite/namespace/repository_test.go b/infrastructure/sqlite/namespace/repository_test.go index 7477352..1656f36 100644 --- a/infrastructure/sqlite/namespace/repository_test.go +++ b/infrastructure/sqlite/namespace/repository_test.go @@ -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) } diff --git a/infrastructure/sqlite/namespace/schema.sql b/infrastructure/sqlite/namespace/schema.sql new file mode 100644 index 0000000..47cdb39 --- /dev/null +++ b/infrastructure/sqlite/namespace/schema.sql @@ -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 + ); diff --git a/sqlc.yaml b/sqlc.yaml new file mode 100644 index 0000000..8445230 --- /dev/null +++ b/sqlc.yaml @@ -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