dalgo2postgres

package module
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 4, 2026 License: MIT Imports: 17 Imported by: 1

README

dalgo2postgres

PostgreSQL-specific DALgo driver. Wraps github.com/dal-go/dalgo2sql to provide the dal.DB surface, and adds PostgreSQL-native implementations of:

  • dbschema.SchemaReader — schema introspection via information_schema and pg_indexes
  • ddl.Applier — PostgreSQL-flavored CREATE TABLE / CREATE INDEX / DROP TABLE / ALTER TABLE
  • dal.ConcurrencyAware — advertises SupportsConcurrentConnections() = true (PostgreSQL supports concurrent connections from multiple goroutines and processes, unlike SQLite which serializes writers)

Constructor usage

import (
    "github.com/dal-go/dalgo/dal"
    "github.com/dal-go/dalgo2postgres"
    "github.com/dal-go/dalgo2sql"
)

// Minimal — no per-collection primary-key configuration.
db, err := dalgo2postgres.NewDatabase("postgres://user:pass@localhost:5432/mydb?sslmode=disable")

// With per-collection recordset options (required for Insert/Get/Delete with map[string]any data):
db, err := dalgo2postgres.NewDatabaseWithOptions(dsn, dal.NewSchema(nil, nil),
    dalgo2sql.DbOptions{
        Recordsets: map[string]*dalgo2sql.Recordset{
            "widgets": dalgo2sql.NewRecordset("widgets", dalgo2sql.Table,
                []dal.FieldRef{dal.Field("id")}),
        },
    })
if err != nil {
    return err
}
defer db.Close()

Schema reader options

The schema reader (ListCollections, DescribeCollection, ListIndexes, ListConstraints, ListReferrers) lists tables and views of one PostgreSQL schema. Both constructors take optional settings:

db, err := dalgo2postgres.NewDatabase(dsn,
    dalgo2postgres.WithSchema("sales"),                              // default "public"
    dalgo2postgres.WithIdentifierMode(dalgo2postgres.IdentifierExact)) // default IdentifierFoldLower
  • IdentifierFoldLower (default) lower-cases a table name before looking it up, matching the lower-cased tables this package's DDL creates. It is the default so that callers that pass no option and create their tables through this package keep working. A caller that reads a database it did not create with this package must pass IdentifierExact explicitly: with the default, ListCollections reports Album but DescribeCollection("Album") looks for album and answers not found.
  • IdentifierExact looks the table up under exactly the name given, so a table created as "Album" is found as Album and not as album.
  • A nil option is ignored; an unknown IdentifierMode makes the constructor return an error before it connects.

A collection reference that names its own schema (dal.NewQualifiedRootCollectionRef("sales", "Album", "")) is read from that schema, whatever WithSchema says; a reference without a schema is read from the configured one. The reader never answers a reference from a schema it does not name. ListReferrers reports only referrers in the same schema as the queried table; a foreign key into another schema shows up in DescribeCollection as ReferencedNamespace, which is empty when the target is in the table's own schema.

NonDeterministicTextColumns(ctx, ref) lists the columns of a table or view whose equality is not exact (type citext, or a non-deterministic collation such as a case-insensitive ICU collation), in column order. It addresses the table exactly as the schema reader does (reference schema, else WithSchema; name per IdentifierMode), returns an empty list when every column is exact, and a not-found error when the table does not exist, so an empty list always means "exists and exact". A domain over citext and an array of citext are not reported.

WithSchema and IdentifierExact affect only the schema reader. The DDL this package writes still lower-cases every name (see "Identifier case folding" below), and record operations (Get, Insert, queries) address tables as dalgo2sql renders them and resolve them through the connection's search_path.

ListReferrers returns one Referrer per foreign key, with the referencing columns in key order.

A column whose type has no dbschema.Type (uuid, json, jsonb, arrays, enums, interval, inet, money, ...) is described as a String field; it never fails the table. Base tables and views are listed; dbschema has no collection kind, so a view cannot be told apart from a table in the result. Materialized views and foreign tables are not listed (PostgreSQL does not expose materialized views in information_schema.tables, and foreign tables are filtered out).

Type mapping

dbschema.Type PostgreSQL column type
String TEXT (or VARCHAR(n) when Length is set)
Int BIGINT
Float DOUBLE PRECISION
Bool BOOLEAN
Time TIMESTAMPTZ
Decimal NUMERIC (or NUMERIC(total, scale) when Precision is set)
Bytes BYTEA

Running the tests

The tests that touch a live database are gated on the DALGO2POSTGRES_TEST_DSN environment variable. When it is not set, those tests are skipped so plain CI passes without a database.

Start a local PostgreSQL 17 server (Docker):

docker run -d \
  --name postgres17 \
  -e POSTGRES_USER=ovdb \
  -e POSTGRES_PASSWORD=ovdb \
  -e POSTGRES_DB=ovdb \
  -p 15432:5432 \
  postgres:17

Then run the tests:

DALGO2POSTGRES_TEST_DSN='postgres://ovdb:ovdb@127.0.0.1:15432/ovdb?sslmode=disable' \
    go test ./... -count=1 -v

PostgreSQL driver: github.com/jackc/pgx/v5/stdlib (pure Go, CGO_ENABLED=0)

This package uses github.com/jackc/pgx/v5/stdlib — a pure-Go PostgreSQL driver exposed through the standard database/sql interface (driver name "pgx"). No C toolchain is required; the package compiles with CGO_ENABLED=0.

Placeholder dialect

PostgreSQL requires positional parameter markers $1, $2, … rather than the ? style used by SQLite and MySQL. dalgo2postgres automatically sets dalgo2sql.DbOptions.Placeholder = dalgo2sql.PlaceholderDollar so that all SQL emitted through dalgo2sql uses the correct form. This field was added to dalgo2sql as a minimal backward-compatible extension; the zero value (PlaceholderQuestion) preserves the existing behavior for all other drivers.

Duplicate-key classification

record.ErrRecordExists / record.IsAlreadyExists let callers detect an Insert over an existing key without depending on driver-specific error types or message text. dalgo2sql owns the wrapping (it applies fmt.Errorf("%w: %w", record.ErrRecordExists, err) at its single execInsert choke point, covering Insert, InsertMulti, and their transactional variants) but delegates the one question only a driver knows the answer to — is this particular error a unique-key violation? — to dalgo2sql.DbOptions.IsAlreadyExists.

dalgo2postgres supplies that hook: NewDatabase and NewDatabaseWithOptions default DbOptions.IsAlreadyExists to dalgo2postgres.IsAlreadyExists whenever the caller leaves it nil (a caller-supplied hook is never overridden). It reports true when the error is a *pgconn.PgError (from github.com/jackc/pgx/v5/pgconn, the driver this package uses) whose Code is the PostgreSQL SQLSTATE 23505 (unique_violation) — matched on the SQLSTATE code via errors.As, never the error message. Sibling class-23 codes such as 23503 (foreign_key_violation) and 23514 (check_violation) are integrity violations but not duplicate keys, and are deliberately reported as false.

dalgo2postgres.IsAlreadyExists is exported so callers who construct their own dalgo2sql.DbOptions directly (e.g. calling dalgo2sql.NewDatabase themselves) can reuse it instead of rewriting the same *pgconn.PgError check.

Identifier case folding

PostgreSQL folds unquoted identifiers to lower case. dalgo2postgres quotes identifiers (so reserved words and otherwise-illegal names stay usable) but lower-cases them first, so the case-preserving quoted form agrees with the unquoted references that dalgo2sql's DML and dalgo's structured-query rendering emit (which PostgreSQL also folds to lower case). DDL and DML thus always address the same physical identifier.

Consequence: collection (table) and field (column) names are stored lower-cased. Typed clients round-trip transparently because encoding/json unmarshalling is case-insensitive; consumers reading raw records observe lower-cased field names. Fully case-preserving storage would require dalgo's structured-query String() to quote column identifiers, tracked upstream.

Documentation

Overview

Package dalgo2postgres is the PostgreSQL-specific DALgo driver.

It composes github.com/dal-go/dalgo2sql for the dal.DB read/write surface (transactions, recordset reader, Get/Set/Insert/Delete) and adds PostgreSQL-native implementations of:

Index

Constants

View Source
const DefaultSchema = "public"

DefaultSchema is the PostgreSQL schema the schema reader inspects when WithSchema is not used.

Variables

View Source
var Version = resolveVersion(debug.ReadBuildInfo)

Version is the module version recorded in the consuming binary's build information (a tag or a pseudo-version), or "devel" when none is recorded, for example in a local-path replace or a working-tree build. Treat it as read-only.

Functions

func IsAlreadyExists added in v0.2.1

func IsAlreadyExists(err error) bool

IsAlreadyExists is a dalgo2sql.DbOptions.IsAlreadyExists classifier for PostgreSQL via github.com/jackc/pgx/v5 (the driver this package uses — see the "pgx" import in database.go). It reports whether err represents a unique-key violation: the driver reports these as a *pgconn.PgError whose Code is the SQLSTATE "23505" (unique_violation).

Detection matches the SQLSTATE code via errors.As, never the error message — messages are not a stable contract and can vary by locale or server version. A sibling class-23 code such as "23503" (foreign_key_violation) or "23514" (check_violation) is a different kind of constraint failure, not a duplicate key, and is deliberately reported as false here.

NewDatabaseWithOptions (and therefore NewDatabase) wires this in automatically whenever the caller's dalgo2sql.DbOptions.IsAlreadyExists is nil. It is exported so callers who build a dalgo2sql.DbOptions directly — e.g. calling dalgo2sql.NewDatabase themselves rather than going through this package's constructors — can reuse it instead of rewriting the same *pgconn.PgError check.

Types

type Database

type Database struct {
	dal.ConcurrencyAvailable // SupportsConcurrentConnections() = true

	dal.DB // delegate for the dal.DB surface
	// contains filtered or unexported fields
}

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.

func NewDatabase

func NewDatabase(dsn string, options ...Option) (*Database, error)

NewDatabase opens a connection to the PostgreSQL server identified by dsn using github.com/jackc/pgx/v5/stdlib (pure Go, CGO_ENABLED=0), pings to surface connectivity errors at construction time, wraps the *sql.DB via dalgo2sql.NewDatabase for the dal.DB surface, and returns a *Database that satisfies dal.DB + dal.ConcurrencyAware.

Use NewDatabaseWithOptions when you need to supply per-collection primary-key metadata (required for Insert/Get/Delete with map[string]any data). The optional Option values choose the schema and table-name matching the schema reader uses.

func NewDatabaseWithOptions

func NewDatabaseWithOptions(dsn string, schema dal.Schema, opts dalgo2sql.DbOptions, options ...Option) (*Database, error)

NewDatabaseWithOptions is like NewDatabase but accepts a dal.Schema and dalgo2sql.DbOptions so callers can configure per-collection primary-key mappings required by Insert/Get/Delete operations.

Example — open a DB whose "widgets" table has "id" as its primary key:

db, err := dalgo2postgres.NewDatabaseWithOptions(dsn, dal.NewSchema(nil, nil),
    dalgo2sql.DbOptions{
        Recordsets: map[string]*dalgo2sql.Recordset{
            "widgets": dalgo2sql.NewRecordset("widgets", dalgo2sql.Table,
                []dal.FieldRef{dal.Field("id")}),
        },
    })

The optional Option values choose the PostgreSQL schema the schema reader inspects (WithSchema) and whether it matches table names exactly (WithIdentifierMode). A nil option is ignored; an unknown IdentifierMode is an error, returned before any connection is attempted.

func (*Database) Adapter

func (d *Database) Adapter() dal.Adapter

Adapter returns the driver/version identifier.

func (*Database) AlterCollection

func (d *Database) AlterCollection(ctx context.Context, name string, ops ...ddl.AlterOp) error

AlterCollection applies ops in order inside a single transaction. Partial failures roll back and leave the collection untouched.

func (*Database) Close

func (d *Database) Close() error

Close closes the underlying *sql.DB. After Close the Database value is unusable; further method calls will fail with an error from database/sql.

func (*Database) CreateCollection

func (d *Database) CreateCollection(ctx context.Context, c dbschema.CollectionDef, opts ...ddl.Option) error

CreateCollection creates a table and its inline indexes transactionally. On any error, the transaction rolls back and no schema state remains.

func (*Database) Delete

func (d *Database) Delete(ctx context.Context, key *dalrecord.Key) error

func (*Database) DeleteMulti

func (d *Database) DeleteMulti(ctx context.Context, keys []*dalrecord.Key) error

func (*Database) DescribeCollection

func (d *Database) DescribeCollection(ctx context.Context, ref *dal.CollectionRef) (*dbschema.CollectionDef, error)

DescribeCollection returns the full schema definition for the named table or view. It queries information_schema for columns and primary-key membership. The table is read from the schema the reference names, else from the configured schema. A column whose PostgreSQL type has no dbschema counterpart (uuid, json, jsonb, arrays, enums, interval, inet, money, ...) is reported as a String field rather than failing the whole table.

func (*Database) DropCollection

func (d *Database) DropCollection(ctx context.Context, name string, opts ...ddl.Option) error

DropCollection drops the table and all its indexes (Postgres cascades automatically).

func (*Database) ExecuteQueryToRecordsReader

func (d *Database) ExecuteQueryToRecordsReader(ctx context.Context, query dal.Query) (dal.RecordsReader, error)

func (*Database) ExecuteQueryToRecordsetReader

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

func (*Database) Exists

func (d *Database) Exists(ctx context.Context, key *dalrecord.Key) (bool, error)

func (*Database) Get

func (d *Database) Get(ctx context.Context, record dalrecord.Record) error

func (*Database) GetMulti

func (d *Database) GetMulti(ctx context.Context, records []dalrecord.Record) error

func (*Database) ID

func (d *Database) ID() string

ID returns the driver-issued database ID (delegated to dalgo2sql).

func (*Database) Insert

func (d *Database) Insert(ctx context.Context, record dalrecord.Record, opts ...dal.InsertOption) error

func (*Database) ListCollections

func (d *Database) ListCollections(ctx context.Context, parent *dalrecord.Key) ([]dal.CollectionRef, error)

ListCollections returns the tables and views of the configured schema (DefaultSchema unless WithSchema says otherwise) in alphabetical order, under the names PostgreSQL reports. The parent *dalrecord.Key is ignored — Postgres has a flat table namespace within a schema.

func (*Database) ListConstraints

func (d *Database) ListConstraints(ctx context.Context, ref *dal.CollectionRef) ([]dbschema.ConstraintDef, error)

ListConstraints returns a best-effort survey of constraints on the table via information_schema, in the schema the reference names, else the configured schema:

  • PRIMARY KEY constraint (one row if any PK columns exist)
  • UNIQUE constraints
  • FOREIGN KEY constraints

CHECK constraints are NOT enumerated here; read them from DescribeCollection if needed.

func (*Database) ListIndexes

func (d *Database) ListIndexes(ctx context.Context, ref *dal.CollectionRef) ([]dbschema.IndexDef, error)

ListIndexes returns the non-primary-key indexes on the named table via pg_indexes, from the schema the reference names, else the configured schema.

func (*Database) ListReferrers

func (d *Database) ListReferrers(ctx context.Context, ref *dal.CollectionRef) ([]dbschema.Referrer, error)

ListReferrers returns the foreign keys that reference ref, one Referrer per foreign key, with the referencing columns in key order. Only referrers in the schema of ref (the schema the reference names, else the configured one) are reported, because the reader addresses a single schema. It reads pg_catalog rather than information_schema: constraint names are unique only per table in PostgreSQL, so information_schema joins on the name alone mix up tables that reuse a name.

func (*Database) NonDeterministicTextColumns added in v0.3.0

func (d *Database) NonDeterministicTextColumns(ctx context.Context, ref *dal.CollectionRef) ([]string, error)

NonDeterministicTextColumns returns the columns of the referenced table (or view) whose equality is not exact: columns of type citext and columns with a non-deterministic collation (for example a case-insensitive ICU collation), in column order. Under such a column "A" = "a" can be true, so a comparison or a group-by on it does not mean what a byte-for-byte match means; callers use this list to know where a pushed-down comparison differs from Go's.

The table is looked up in the schema the reference names (dal.NewQualifiedRootCollectionRef), else in the Database's schema (DefaultSchema unless WithSchema says otherwise), and its name is matched as the schema reader matches it (see IdentifierMode). Both are sent as bound parameters. A table with no such column yields an empty non-nil slice. A table that does not exist (under the name as IdentifierMode resolves it) yields a "not found" error, never an empty list, so an empty result means the table exists and none of its columns is inexact in the sense below.

The match is on the column's own type and collation. A column whose type is a domain over citext, or an array of citext, is not reported; treat such columns as inexact yourself.

func (*Database) RunReadonlyTransaction

func (d *Database) RunReadonlyTransaction(ctx context.Context, f dal.ROTxWorker, opts ...dal.TransactionOption) error

func (*Database) RunReadwriteTransaction

func (d *Database) RunReadwriteTransaction(ctx context.Context, f dal.RWTxWorker, opts ...dal.TransactionOption) error

func (*Database) Schema

func (d *Database) Schema() dal.Schema

Schema returns the dal-level Schema (delegated to dalgo2sql).

func (*Database) Set

func (d *Database) Set(ctx context.Context, record dalrecord.Record) error

func (*Database) SetMulti

func (d *Database) SetMulti(ctx context.Context, records []dalrecord.Record) error

func (*Database) SupportsConcurrentConnections added in v0.2.0

func (d *Database) SupportsConcurrentConnections() bool

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 (*Database) SupportsTransactionalDDL

func (d *Database) SupportsTransactionalDDL() bool

SupportsTransactionalDDL reports that PostgreSQL supports transactional DDL — every CREATE / DROP / ALTER statement can be wrapped in a BEGIN/COMMIT and is rolled back atomically on commit failure.

func (*Database) Update

func (d *Database) Update(ctx context.Context, key *dalrecord.Key, updates []update.Update, preconditions ...dal.Precondition) error

func (*Database) UpdateMulti

func (d *Database) UpdateMulti(ctx context.Context, keys []*dalrecord.Key, updates []update.Update, preconditions ...dal.Precondition) error

func (*Database) UpdateRecord

func (d *Database) UpdateRecord(ctx context.Context, record dalrecord.Record, updates []update.Update, preconditions ...dal.Precondition) error

UpdateRecord is not supported at the database level by dalgo2sql; use Update with an explicit key instead, or call UpdateRecord inside a RunReadwriteTransaction where the transaction object does support it.

func (*Database) Upsert

func (d *Database) Upsert(ctx context.Context, record dalrecord.Record) error

type IdentifierMode added in v0.2.4

type IdentifierMode int

IdentifierMode says how the schema reader treats the table names it is given.

const (
	// IdentifierFoldLower lower-cases a table name before looking it up. It
	// matches the DDL this package writes (see quoteIdent), which stores every
	// name lower-cased. It is the default, so callers that pass no option and
	// create their tables through this package keep their behaviour. A caller
	// that reads a database it did not create with this package (mixed-case
	// table names) must choose [IdentifierExact] explicitly: with the default,
	// ListCollections reports "Album" but DescribeCollection("Album") looks for
	// "album" and answers not found.
	IdentifierFoldLower IdentifierMode = iota

	// IdentifierExact looks a table up under exactly the name it is given.
	// PostgreSQL names are case-sensitive once quoted, so "Album" and "album"
	// are different tables; choose this mode to read a database whose tables
	// were created with mixed-case names.
	//
	// The mode affects only the schema reader (ListCollections,
	// DescribeCollection, ListIndexes, ListConstraints, ListReferrers). The DDL
	// this package writes still lower-cases every name, and record operations
	// address tables as dalgo2sql renders them, resolved by the connection's
	// search_path.
	IdentifierExact
)

type Option added in v0.2.4

type Option func(*Database)

Option customises how a Database reads its schema.

func WithIdentifierMode added in v0.2.4

func WithIdentifierMode(mode IdentifierMode) Option

WithIdentifierMode selects how table names given to the schema reader are matched. See IdentifierMode.

func WithSchema added in v0.2.4

func WithSchema(name string) Option

WithSchema makes the schema reader inspect the named PostgreSQL schema instead of DefaultSchema. An empty name keeps the default. The name is always sent to the server as a bound parameter, never inside SQL text.

A collection reference that names its own schema (dal.NewQualifiedRootCollectionRef) is read from that schema instead.

The option affects only the schema reader (ListCollections, DescribeCollection, ListIndexes, ListConstraints, ListReferrers). The DDL this package writes and record operations still follow the connection's search_path.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL