Compare commits

..

No commits in common. "912210cc90e3f9b688fe3ca5f08fd6912f63f9e2" and "8c6d8354df3a70f5037900eb672b6a85f505042e" have entirely different histories.

5 changed files with 11 additions and 164 deletions

View File

@ -58,10 +58,9 @@ linters:
checks: checks:
- all - all
- -ST1000 # Re-enable this once we have docstrings - -ST1000 # Re-enable this once we have docstrings
- -ST1001 # Dot imports are good sometimes (e.g., in test packages)
- -ST1003 # I like snake_case - -ST1003 # I like snake_case
- -ST1013 # HTTP status codes are shorter and more readable than names - -ST1013 # HTTP status codes are shorter and more readable than names
dot-import-whitelist:
- "git.offline-twitter.com/offline-labs/gas-stack/pkg/flowutils"
exclusions: exclusions:
generated: lax # Don't lint generated files generated: lax # Don't lint generated files
paths: paths:

View File

@ -1,8 +1,8 @@
TODO: auto-timestamps TODO: auto-timestamps
- SaveXyz should set created_at and updated_at; shouldn't touch is_deleted or deleted_at - SaveXyz should set created_at and updated_at; shouldn't touch is_deleted or deleted_at
- if soft delete is enabled, DeleteXyz should do update (not delete) and set is_deleted and deleted_at - if soft delete is enabled, DeleteXyz should do update (not delete) and set is_deleted and deleted_at
- ...and DeleteXyz should have pointer receiver for soft-delete
- SaveXyz shouldn't set created_at in the do-update branch - SaveXyz shouldn't set created_at in the do-update branch
- DeleteXyz should have pointer receiver
- GetXyzByID should include `ErrItemIsDeleted` if item is soft-deleted - GetXyzByID should include `ErrItemIsDeleted` if item is soft-deleted
TODO: primary-key TODO: primary-key

View File

@ -22,6 +22,10 @@ var (
version_number uint version_number uint
) )
var (
ErrTargetExists = errors.New("target already exists")
)
// Colors for terminal output // Colors for terminal output
const ( const (
ColorReset = "\033[0m" ColorReset = "\033[0m"
@ -47,39 +51,22 @@ func Create(path string) (*sqlx.DB, error) {
// First check if the path already exists // First check if the path already exists
_, err := os.Stat(path) _, err := os.Stat(path)
if err == nil { if err == nil {
return nil, ErrDatabaseAlreadyExists return nil, ErrTargetExists
} else if !errors.Is(err, os.ErrNotExist) { } else if !errors.Is(err, os.ErrNotExist) {
return nil, fmt.Errorf("path error: %w", err) return nil, fmt.Errorf("path error: %w", err)
} }
// Create DB file // Create DB file
fmt.Printf("Creating............. %s\n", path) fmt.Printf("Creating............. %s\n", path)
db, err := sqlx.Open("sqlite3", path) db := sqlx.MustOpen("sqlite3", path+"?_foreign_keys=on&_journal_mode=WAL")
if err != nil { db.MustExec(*sql_schema)
return nil, fmt.Errorf("opening db: %w", err)
}
// Initialize schema
if _, err = db.Exec("pragma foreign_keys=on; pragma journal_mode=WAL;"); err != nil {
return nil, fmt.Errorf("running pragma statements: %w", err)
}
if _, err = db.Exec(*sql_schema); err != nil {
return nil, fmt.Errorf("creating schema: %w", err)
}
return db, nil return db, nil
} }
func Connect(path string) (*sqlx.DB, error) { func Connect(path string) (*sqlx.DB, error) {
db, err := sqlx.Open("sqlite3", path) db := sqlx.MustOpen("sqlite3", fmt.Sprintf("%s?_foreign_keys=on&_journal_mode=WAL", path))
if err != nil { err := CheckAndUpdateVersion(db)
return nil, fmt.Errorf("opening db: %w", err)
}
if _, err = db.Exec("pragma foreign_keys=on; pragma journal_mode=WAL;"); err != nil {
return nil, fmt.Errorf("running pragma statements: %w", err)
}
err = CheckAndUpdateVersion(db)
return db, err return db, err
} }

View File

@ -1,75 +0,0 @@
package db
import (
"errors"
"fmt"
"github.com/mattn/go-sqlite3"
)
var (
ErrNotInDB = errors.New("not in db")
ErrItemIsDeleted = errors.New("item is deleted")
ErrForeignKeyViolation = errors.New("foreign key constraint failed")
ErrDatabaseAlreadyExists = errors.New("target already exists")
)
type ForeignKey interface {
~uint64 | ~string
}
type ForeignKeyError[T ForeignKey] struct {
Field string
TargetTable string
FkValue T
}
func NewForeignKeyError[T ForeignKey](field, table string, fkValue T) ForeignKeyError[T] {
return ForeignKeyError[T]{
Field: field,
TargetTable: table,
FkValue: fkValue,
}
}
func (e ForeignKeyError[T]) Error() string {
return fmt.Sprintf(`%s: fk field %q (to %q) with value "%v"`,
ErrForeignKeyViolation.Error(), e.Field, e.TargetTable, e.FkValue,
)
}
// Unwrap returns the sentinel error-- permits the use of errors.Is for convenience
func (e ForeignKeyError[T]) Unwrap() error {
return ErrForeignKeyViolation
}
// -------------
// SQLite errors
// -------------
// IsSqliteFkError checks whether an error is a SQLite foreign key constraint violation.
func IsSqliteFkError(err error) bool {
var sqliteErr sqlite3.Error
if !errors.As(err, &sqliteErr) {
return false
}
return errors.Is(sqliteErr.ExtendedCode, sqlite3.ErrConstraintForeignKey)
}
// IsSqliteUniqError checks whether an error is a SQLite unique constraint violation.
func IsSqliteUniqError(err error) bool {
var sqliteErr sqlite3.Error
if !errors.As(err, &sqliteErr) {
return false
}
return errors.Is(sqliteErr.ExtendedCode, sqlite3.ErrConstraintUnique)
}
// IsSqliteNotNullError checks whether an error is a SQLite not null constraint violation.
func IsSqliteNotNullError(err error) bool {
var sqliteErr sqlite3.Error
if !errors.As(err, &sqliteErr) {
return false
}
return errors.Is(sqliteErr.ExtendedCode, sqlite3.ErrConstraintNotNull)
}

View File

@ -1,64 +0,0 @@
package db
import (
"database/sql/driver"
"fmt"
"strconv"
"time"
)
// Timestamp is a type that wraps `time.Time`. It implements `driver.Value` and `json.Marshal` to
// store its value as an integer, a Unix timestamp in milliseconds.
//
// It uses type embedding (`struct { time.Time }`), instead of making it simply a typedef of
// `time.Time`, because that keeps methods on `time.Time` (like Add, UnixMilli, etc.) intact and
// exposed on instances of Timestamp.
type Timestamp struct {
time.Time
}
func TimestampFromUnix(num int64) Timestamp {
return Timestamp{time.Unix(num, 0)}
}
func TimestampFromUnixMilli(num int64) Timestamp {
return Timestamp{time.UnixMilli(num)}
}
// TimestampNow returns a new Timestamp corresponding to the current time, rounded to the nearest millisecond.
func TimestampNow() Timestamp {
return Timestamp{time.Now().Round(time.Millisecond)}
}
// ------------
// driver.Value
// ------------
func (t Timestamp) Value() (driver.Value, error) {
return t.UnixMilli(), nil
}
func (t *Timestamp) Scan(src any) error {
val, isOk := src.(int64)
if !isOk {
return fmt.Errorf("incompatible type for Timestamp: %#v", src)
}
*t = TimestampFromUnixMilli(val)
return nil
}
// ------------------------
// json.Marshal / Unmarshal
// ------------------------
func (t Timestamp) MarshalJSON() ([]byte, error) {
return fmt.Appendf(nil, "%d", t.UnixMilli()), nil
}
func (t *Timestamp) UnmarshalJSON(b []byte) error {
ms, err := strconv.ParseInt(string(b), 10, 64)
if err != nil {
return fmt.Errorf("invalid timestamp %q: %w", string(b), err)
}
*t = TimestampFromUnixMilli(ms)
return nil
}