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
47 changes: 47 additions & 0 deletions conformance_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package dalgo2postgres

import (
"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 live PostgreSQL
// server, the same as every other DB-backed test in this package: it needs
// DALGO2POSTGRES_TEST_DSN (see testDSN in database_test.go) and skips when
// that is not set.
//
// This repo has no CI service container wired up for PostgreSQL, so as of
// this change the suite has been exercised locally only where
// DALGO2POSTGRES_TEST_DSN happened to be set — it has not been run in CI.
// Wiring a PostgreSQL service container into .github/workflows/ci.yml is a
// separate follow-up.
func TestConformance(t *testing.T) {
tbl := uniqueTable(t, "conformance")
opts := dalgo2sql.DbOptions{
Recordsets: map[string]*dalgo2sql.Recordset{
tbl: dalgo2sql.NewRecordset(tbl, dalgo2sql.Table, []dal.FieldRef{dal.Field("ID")}),
},
}

// Column case matches dalgotest.Record's Go field names (ID, Name):
// dalgo2sql derives column names from the struct via reflection.
// PostgreSQL folds unquoted identifiers to lower case (see quoteIdent),
// so the raw DDL below and the unquoted identifiers dalgo2sql's DML
// emits fold to the same physical column regardless of the case used
// here.
setup := openTestDBWithOpts(t, opts)
if _, err := setup.sqlDB.Exec(`CREATE TABLE ` + quoteIdent(tbl) + ` (
ID VARCHAR(255) NOT NULL PRIMARY KEY,
Name VARCHAR(255)
)`); err != nil {
t.Fatalf("CREATE TABLE: %v", err)
}
t.Cleanup(func() { dropTable(t, setup, tbl) })

dalgotest.RunConformance(t, func(t *testing.T) (dal.DB, func()) {
return openTestDBWithOpts(t, opts), nil
}, dalgotest.WithCollection(tbl))
}
38 changes: 26 additions & 12 deletions database.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,24 @@ import (
_ "github.com/jackc/pgx/v5/stdlib" // register the "pgx" driver (pure Go, CGO_ENABLED=0)
)

// Database is the dalgo2postgres driver instance. It implements
// [dal.DB] by delegating to an inner [dal.DB] obtained from
// [dalgo2sql.NewDatabase], and adds PostgreSQL-specific dbschema, ddl,
// and concurrency surfaces.
// Database is the dalgo2postgres driver instance. It implements [dal.DB] by
// embedding a [dal.DB] obtained from [dalgo2sql.NewDatabase], and adds
// PostgreSQL-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 — the underlying PostgreSQL server and pgx connection pool both
// support concurrent connections from multiple goroutines.
type Database struct {
dal.ConcurrencyAvailable // SupportsConcurrentConnections() = true

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

// NewDatabase opens a connection to the PostgreSQL server identified by dsn
Expand Down Expand Up @@ -80,9 +84,9 @@ func NewDatabaseWithOptions(dsn string, schema dal.Schema, opts dalgo2sql.DbOpti
}
innerDB := dalgo2sql.NewDatabase(sqlDB, schema, opts)
return &Database{
innerDB: innerDB,
sqlDB: sqlDB,
dsn: dsn,
DB: innerDB,
sqlDB: sqlDB,
dsn: dsn,
}, nil
}

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

// SupportsConcurrentConnections reports PostgreSQL's own concurrency
// behaviour (always true — see dal.ConcurrencyAvailable), not dalgo2sql's.
// An explicit method is required here: dal.ConcurrencyAvailable 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.ConcurrencyAvailable.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("dalgo2postgres", 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 dalgo2postgres 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/dalgo2postgres
go 1.26

require (
github.com/dal-go/dalgo v0.63.1
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
github.com/jackc/pgx/v5 v5.10.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/georgysavva/scany/v2 v2.1.4 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
Expand Down
16 changes: 8 additions & 8 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,17 +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/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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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=
Expand Down