diff --git a/conformance_test.go b/conformance_test.go new file mode 100644 index 0000000..e64c7cc --- /dev/null +++ b/conformance_test.go @@ -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() } + }) +} diff --git a/database.go b/database.go index 13877e8..64af49c 100644 --- a/database.go +++ b/database.go @@ -23,10 +23,14 @@ 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 @@ -34,9 +38,9 @@ import ( 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 @@ -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 } @@ -91,8 +95,18 @@ 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 { @@ -100,7 +114,7 @@ func (d *Database) Adapter() dal.Adapter { } // 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(). diff --git a/database_dal.go b/database_dal.go index 20b7073..a0b6571 100644 --- a/database_dal.go +++ b/database_dal.go @@ -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 @@ -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 diff --git a/go.mod b/go.mod index 0f1175c..8f9457c 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 1d8dc74..228dcb2 100644 --- a/go.sum +++ b/go.sum @@ -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=