Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions conformance_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package dalgo2sqlite

import (
"path/filepath"
"testing"

"github.com/dal-go/dalgo/dal"
"github.com/dal-go/dalgo/dalgotest"
"github.com/dal-go/dalgo2sql"
)

// TestConformance runs the shared dalgotest suite against a fresh SQLite
// file database (modernc.org/sqlite, pure Go) — no external service and no
// env-gate needed, the same as every other test in this package.
//
// dalgo2sqlite itself validates nothing beyond what dalgo2sql provides.
// Every check here passes because dal.NewDB's write pipeline runs
// BeforeSave validation and hooks before RunReadwriteTransaction ever
// reaches dalgo2sql's transaction, and RunReadwriteTransaction is the path
// every check in this suite writes through — see database_dal.go's
// RunReadwriteTransaction and dalgo2sql's own conformance_test.go.
func TestConformance(t *testing.T) {
opts := dalgo2sql.DbOptions{
Recordsets: map[string]*dalgo2sql.Recordset{
dalgotest.DefaultCollection: dalgo2sql.NewRecordset(
dalgotest.DefaultCollection, dalgo2sql.Table, []dal.FieldRef{dal.Field("ID")},
),
},
}

dalgotest.RunConformance(t, func(t *testing.T) (dal.DB, func()) {
db, err := NewDatabaseWithOptions(
filepath.Join(t.TempDir(), "conformance.db"), dal.NewSchema(nil, nil), opts)
if err != nil {
t.Fatalf("NewDatabaseWithOptions: %v", err)
}
if _, err := db.sqlDB.Exec(`CREATE TABLE ` + dalgotest.DefaultCollection + ` (
ID TEXT PRIMARY KEY,
Name TEXT
)`); err != nil {
t.Fatalf("CREATE TABLE: %v", err)
}
return db, func() { _ = db.Close() }
})
}
38 changes: 26 additions & 12 deletions database.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,24 @@ import (
_ "modernc.org/sqlite" // register the "sqlite" driver (pure Go, CGO_ENABLED=0)
)

// Database is the dalgo2sqlite driver instance. It implements
// [dal.DB] by delegating to an inner [dal.DB] obtained from
// [dalgo2sql.NewDatabase], and adds SQLite-specific dbschema, ddl,
// and concurrency surfaces.
// Database is the dalgo2sqlite driver instance. It implements [dal.DB] by
// embedding a [dal.DB] obtained from [dalgo2sql.NewDatabase], and adds
// SQLite-specific dbschema, ddl, and concurrency surfaces.
//
// The embedded dal.DB (rather than a named field) is what lets Database
// satisfy dal.DB itself: dal.DB is sealed by an unexported marker method,
// and embedding is the only way for that method to be promoted onto a
// decorating type — see dal.NewDB's doc comment.
//
// Construct via [NewDatabase]. Database values are safe for
// concurrent use only insofar as SQLite itself is — readers can be
// concurrent under WAL mode; writers serialize.
type Database struct {
dal.NoConcurrency // SupportsConcurrentConnections() = false

innerDB dal.DB // delegate for the dal.DB surface
sqlDB *sql.DB // direct handle for DDL + PRAGMA queries
dbPath string // remembered for diagnostics
dal.DB // delegate for the dal.DB surface
sqlDB *sql.DB // direct handle for DDL + PRAGMA queries
dbPath string // remembered for diagnostics
}

// NewDatabase opens (or creates) the SQLite file at dbPath using
Expand Down Expand Up @@ -75,9 +79,9 @@ func NewDatabaseWithOptions(dbPath string, schema dal.Schema, opts dalgo2sql.DbO
}
innerDB := dalgo2sql.NewDatabase(sqlDB, schema, opts)
return &Database{
innerDB: innerDB,
sqlDB: sqlDB,
dbPath: dbPath,
DB: innerDB,
sqlDB: sqlDB,
dbPath: dbPath,
}, nil
}

Expand All @@ -91,16 +95,26 @@ func (d *Database) Close() error {
return d.sqlDB.Close()
}

// SupportsConcurrentConnections reports SQLite's own concurrency behaviour
// (always false — see dal.NoConcurrency), not dalgo2sql's. An explicit
// method is required here: dal.NoConcurrency and the embedded dal.DB (whose
// Backend requirement embeds dal.ConcurrencyAware) both declare this method
// at the same promotion depth, which Go otherwise treats as an ambiguous
// selector.
func (d *Database) SupportsConcurrentConnections() bool {
return d.NoConcurrency.SupportsConcurrentConnections()
}

// ID returns the driver-issued database ID (delegated to dalgo2sql).
func (d *Database) ID() string { return d.innerDB.ID() }
func (d *Database) ID() string { return d.DB.ID() }

// Adapter returns the driver/version identifier.
func (d *Database) Adapter() dal.Adapter {
return dal.NewAdapter("dalgo2sqlite", Version)
}

// Schema returns the dal-level Schema (delegated to dalgo2sql).
func (d *Database) Schema() dal.Schema { return d.innerDB.Schema() }
func (d *Database) Schema() dal.Schema { return d.DB.Schema() }

// Version is the dalgo2sqlite package version. Updated by hand on
// each release; consumed by Adapter.Version().
Expand Down
91 changes: 73 additions & 18 deletions database_dal.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,38 +15,54 @@ var _ dal.DB = (*Database)(nil)
// --- dal.DB delegation ---

func (d *Database) RunReadonlyTransaction(ctx context.Context, f dal.ROTxWorker, opts ...dal.TransactionOption) error {
return d.innerDB.RunReadonlyTransaction(ctx, f, opts...)
return d.DB.RunReadonlyTransaction(ctx, f, opts...)
}

func (d *Database) RunReadwriteTransaction(ctx context.Context, f dal.RWTxWorker, opts ...dal.TransactionOption) error {
return d.innerDB.RunReadwriteTransaction(ctx, f, opts...)
return d.DB.RunReadwriteTransaction(ctx, f, opts...)
}

func (d *Database) Get(ctx context.Context, record dalrecord.Record) error {
return d.innerDB.Get(ctx, record)
return d.DB.Get(ctx, record)
}

func (d *Database) GetMulti(ctx context.Context, records []dalrecord.Record) error {
return d.innerDB.GetMulti(ctx, records)
return d.DB.GetMulti(ctx, records)
}

func (d *Database) Exists(ctx context.Context, key *dalrecord.Key) (bool, error) {
return d.innerDB.Exists(ctx, key)
return d.DB.Exists(ctx, key)
}

func (d *Database) ExecuteQueryToRecordsReader(ctx context.Context, query dal.Query) (dal.RecordsReader, error) {
return d.innerDB.ExecuteQueryToRecordsReader(ctx, query)
return d.DB.ExecuteQueryToRecordsReader(ctx, query)
}

func (d *Database) ExecuteQueryToRecordsetReader(ctx context.Context, query dal.Query, opts ...recordset.Option) (dal.RecordsetReader, error) {
return d.innerDB.ExecuteQueryToRecordsetReader(ctx, query, opts...)
return d.DB.ExecuteQueryToRecordsetReader(ctx, query, opts...)
}

// --- extra write methods delegated from dalgo2sql ---

// writeDB is the extended interface exposed by dalgo2sql's concrete type.
// Note: UpdateRecord is intentionally excluded — dalgo2sql's database type
// implements it only on transactions, not on the top-level database object.
// writeDB is the extended interface exposed by dalgo2sql's concrete backend
// type. Note: UpdateRecord is intentionally excluded — dalgo2sql's database
// type implements it only on transactions, not on the top-level database
// object.
//
// None of these methods are part of dal.Backend, and dalgo2sql's backend
// does not satisfy dal.WriteSession in full at the database level either
// (it has no database-level InsertMulti or UpdateRecord), so dal.NewDB never
// wraps it in the validating write pipeline at this level: d.DB's dynamic
// type has none of these methods at all, and a plain "d.DB.(writeDB)"
// assertion always fails. dal.As is what recovers the concrete backend that
// does implement them, via dal.BackendOf — visibly and deliberately, the same
// way dal.WithoutValidation recovers an unvalidated write session.
//
// These direct, non-transactional writes were never run through validation
// before the sealed dal.DB change either, since dalgo2sql's database-level
// write path has no BeforeSave call of its own. Writes made through
// RunReadwriteTransaction above are validated, because that goes through
// d.DB's own RunReadwriteTransaction.
type writeDB interface {
Set(ctx context.Context, record dalrecord.Record) error
SetMulti(ctx context.Context, records []dalrecord.Record) error
Expand All @@ -58,36 +74,75 @@ type writeDB interface {
UpdateMulti(ctx context.Context, keys []*dalrecord.Key, updates []update.Update, preconditions ...dal.Precondition) error
}

// backendWriter recovers writeDB from dalgo2sql's concrete backend via
// dal.As — see the writeDB doc comment above for why a plain assertion
// against d.DB itself cannot reach it.
func (d *Database) backendWriter() (writeDB, bool) {
return dal.As[writeDB](d.DB)
}

func (d *Database) Set(ctx context.Context, record dalrecord.Record) error {
return d.innerDB.(writeDB).Set(ctx, record)
w, ok := d.backendWriter()
if !ok {
return dal.ErrNotImplementedYet
}
return w.Set(ctx, record)
}

func (d *Database) SetMulti(ctx context.Context, records []dalrecord.Record) error {
return d.innerDB.(writeDB).SetMulti(ctx, records)
w, ok := d.backendWriter()
if !ok {
return dal.ErrNotImplementedYet
}
return w.SetMulti(ctx, records)
}

func (d *Database) Insert(ctx context.Context, record dalrecord.Record, opts ...dal.InsertOption) error {
return d.innerDB.(writeDB).Insert(ctx, record, opts...)
w, ok := d.backendWriter()
if !ok {
return dal.ErrNotImplementedYet
}
return w.Insert(ctx, record, opts...)
}

func (d *Database) Upsert(ctx context.Context, record dalrecord.Record) error {
return d.innerDB.(writeDB).Upsert(ctx, record)
w, ok := d.backendWriter()
if !ok {
return dal.ErrNotImplementedYet
}
return w.Upsert(ctx, record)
}

func (d *Database) Delete(ctx context.Context, key *dalrecord.Key) error {
return d.innerDB.(writeDB).Delete(ctx, key)
w, ok := d.backendWriter()
if !ok {
return dal.ErrNotImplementedYet
}
return w.Delete(ctx, key)
}

func (d *Database) DeleteMulti(ctx context.Context, keys []*dalrecord.Key) error {
return d.innerDB.(writeDB).DeleteMulti(ctx, keys)
w, ok := d.backendWriter()
if !ok {
return dal.ErrNotImplementedYet
}
return w.DeleteMulti(ctx, keys)
}

func (d *Database) Update(ctx context.Context, key *dalrecord.Key, updates []update.Update, preconditions ...dal.Precondition) error {
return d.innerDB.(writeDB).Update(ctx, key, updates, preconditions...)
w, ok := d.backendWriter()
if !ok {
return dal.ErrNotImplementedYet
}
return w.Update(ctx, key, updates, preconditions...)
}

func (d *Database) UpdateMulti(ctx context.Context, keys []*dalrecord.Key, updates []update.Update, preconditions ...dal.Precondition) error {
return d.innerDB.(writeDB).UpdateMulti(ctx, keys, updates, preconditions...)
w, ok := d.backendWriter()
if !ok {
return dal.ErrNotImplementedYet
}
return w.UpdateMulti(ctx, keys, updates, preconditions...)
}

// UpdateRecord is not supported at the database level by dalgo2sql; use
Expand Down
8 changes: 4 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ module github.com/dal-go/dalgo2sqlite
go 1.25.0

require (
github.com/dal-go/dalgo v0.63.2
github.com/dal-go/dalgo2sql v0.9.7
github.com/dal-go/record v0.1.0
github.com/dal-go/dalgo v0.64.2
github.com/dal-go/dalgo2sql v0.10.0
github.com/dal-go/record v0.1.1
modernc.org/sqlite v1.54.0
)

require (
github.com/RoaringBitmap/roaring/v2 v2.22.0 // indirect
github.com/RoaringBitmap/roaring/v2 v2.24.0 // indirect
github.com/bits-and-blooms/bitset v1.24.6 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/georgysavva/scany/v2 v2.1.4 // indirect
Expand Down
18 changes: 8 additions & 10 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/RoaringBitmap/roaring/v2 v2.22.0 h1:aGqjvTSkJSTP7W6q518EHiK9RRRb5gJbCaaciCFr/Lg=
github.com/RoaringBitmap/roaring/v2 v2.22.0/go.mod h1:SfT3of9nYh3vis1dIbCj4Yw6KQGujTN+f345nrN/0JA=
github.com/RoaringBitmap/roaring/v2 v2.24.0 h1:zQkkBZtG3WRP4j+P3A5DO221SvL1Br88TJkhyqEQRZo=
github.com/RoaringBitmap/roaring/v2 v2.24.0/go.mod h1:SfT3of9nYh3vis1dIbCj4Yw6KQGujTN+f345nrN/0JA=
github.com/bits-and-blooms/bitset v1.24.6 h1:qcrftZUVBIwfs+m+nhoCBAPT+ZPZZjti8SbHbDQQkZ4=
github.com/bits-and-blooms/bitset v1.24.6/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/cockroachdb/cockroach-go/v2 v2.2.0 h1:/5znzg5n373N/3ESjHF5SMLxiW4RKB05Ql//KWfeTFs=
github.com/cockroachdb/cockroach-go/v2 v2.2.0/go.mod h1:u3MiKYGupPPjkn3ozknpMUpxPaNLTFWAya419/zv6eI=
github.com/dal-go/dalgo v0.63.1 h1:GEJAGlNH5xGLdFasSIRrYdqGfd0+4A9DGQ843qJQbyA=
github.com/dal-go/dalgo v0.63.1/go.mod h1:LtD5XVzb1kAdXaRcWVNy4F2ROC4fqR4jqD3q/GD4fJQ=
github.com/dal-go/dalgo v0.63.2 h1:L3hDte5QaorZngNK619V1zKach4qfeb9jOQQUsn4vrc=
github.com/dal-go/dalgo v0.63.2/go.mod h1:LtD5XVzb1kAdXaRcWVNy4F2ROC4fqR4jqD3q/GD4fJQ=
github.com/dal-go/dalgo2sql v0.9.7 h1:2m5tDVR6fCZwCq3wezpx8th0eIpsakwQF4vkawqRsIY=
github.com/dal-go/dalgo2sql v0.9.7/go.mod h1:yP20ORPKFDmtNEcHzBfrFVJuJPOCqWLYNYaMdE2nuTQ=
github.com/dal-go/record v0.1.0 h1:hA4143oZwIgtBH/1BRTUEZMCpDfXQr3ONlVXX8USCNg=
github.com/dal-go/record v0.1.0/go.mod h1:quwsVJTT0f6y3Mhx+yHpTobY7luX1M6kyO6fdJ/AFYE=
github.com/dal-go/dalgo v0.64.2 h1:uWCRISCMTpuwjq+VKlymq3kBtdr0c09gcsr75zmeqrw=
github.com/dal-go/dalgo v0.64.2/go.mod h1:PZGzE0AqnaJgPEDTSo7ayfKZaoglhFQ9FxHecF3aQAc=
github.com/dal-go/dalgo2sql v0.10.0 h1:2ijrIkOtKxhKafPIh3zi15av2zYnurC63K61CbfvUl0=
github.com/dal-go/dalgo2sql v0.10.0/go.mod h1:UlGLRAdhq5t0Gg3/GWuZ2/KmKRR5tbM4N5ijOvQIJPM=
github.com/dal-go/record v0.1.1 h1:N2WVDBnm2tOb83h5DJqFyINB5kH0vLjKlYFAgCJWv2g=
github.com/dal-go/record v0.1.1/go.mod h1:quwsVJTT0f6y3Mhx+yHpTobY7luX1M6kyO6fdJ/AFYE=
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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
Expand Down