database

package
v0.27.0 Latest Latest
Warning

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

Go to latest
Published: Sep 6, 2026 License: MIT Imports: 29 Imported by: 0

Documentation

Overview

Package database is the data access contract: one connection, one repository shape, and no ORM.

Queries are plain parameterized SQL, written by hand in the templates `aru make:module` emits, which keeps the query plan predictable and the value always in a placeholder. What this package adds on top is:

  1. an auth.Grant required by every operation (the mandatory path);
  2. tenant scoping taken from the Grant, never from a parameter;
  3. automatic instrumentation into the Collector;
  4. one Open, one pool policy, and each driver in its own module.

The tenant does not live here

auth.Tenant(g) is the single source of a tenant for SQL. It sits in the package that owns the Grant rather than in this one, so that the cache, the filesystem and the scheduler can scope a key by customer without importing the database. The tenant comes from the Grant, never from a path, a body, a query or a header.

Why the drivers are separate modules

In Go there is no optional dependency. A single module carrying pgx, MySQL and SQLite would put all three in the go.sum of every project -- in the build, in the binary, and in the vulnerability surface. That is not hypothetical: the skeleton carried pgx and modernc/sqlite together, and govulncheck found a pgx advisory in a project that could have been SQLite-only.

So each connector is its own module with its own go.mod:

go get github.com/arandu-io/hesape/database/connectors/sqlite   // needs nothing installed
go get github.com/arandu-io/hesape/database/connectors/pgx      // Postgres
go get github.com/arandu-io/hesape/database/connectors/mysql    // MySQL

and the project blank-imports the ones it uses:

import (
    "github.com/arandu-io/hesape/database"
    _ "github.com/arandu-io/hesape/database/connectors/pgx"
    _ "github.com/arandu-io/hesape/database/connectors/sqlite"
)

db, closeDB, err := database.Open(cfg)

Switching engines is a line in .env. The import list is what decides which engines a build can speak at all.

ConnectionFactory is the piece that reads that registry. It lives here rather than under the connectors, and that is what makes the inversion work: a factory that imported the three connector modules to choose between them would put all three back in every go.sum. It never names a driver package -- it looks the dialect up in the registry the connector's init filled in. A project that speaks an engine this framework does not ship registers its own the same way; the ConnectionFactory doc walks through the four lines.

The conformance subpackage is what makes "one Repository, three engines" a measurement rather than a claim: one suite, run by all three connectors against a real server.

Where the Grant is

Repository is the door: every method on it takes an auth.Grant and filters by auth.Tenant(g), on a read exactly as on a write. Connection, DB and the migrator are the plumbing under that door and take none -- not as an exemption, but because a Grant they could not use to filter anything would be a parameter that looks like enforcement and is not. `aru migrate` also runs down here, in a process with no request and no subject, where a Grant cannot be constructed at all.

So a module reaches rows through a Repository. A module that reaches them through a Connection is a module that gets sent back in review.

There is one way to connect

A driver registers itself with database/sql and everything it takes arrives in the DSN. Connector is deliberately the smaller thing -- it says which driver it linked and never opens a connection -- and Open resolves the rest from DATABASE_URL, so there is one way in. There is no fetch mode to set beforehand either: database/sql scans into whatever the caller passed to Scan, so the mode is the destination.

An automatic identifier is read from the statement that caused it, through sql.Result.LastInsertId, by Connection.InsertReturningID. It is one round trip rather than a SELECT afterwards, and it cannot answer about somebody else's row -- on a pool, "the last identifier" is whoever inserted most recently, which is the classic way one request is handed another request's id. The processor asks for it through processors.LastInsertIDConnection, and what implements that is the binding Query builds per builder, not the pool.

Dumping a query does not end the process

github.com/arandu-io/hesape/database/query.Builder.Dump and github.com/arandu-io/hesape/database/query.Builder.DumpRawSQL write the query and hand the builder back. There is no variant that exits afterwards: a library that kills the process is a library nobody can wrap, and a dump that exits is the same function with a way it cannot be tested.

Why there is no schema-state getter

The dump-and-load helpers exist -- github.com/arandu-io/hesape/database/schema.NewMySqlSchemaState and its two siblings -- but they live in the schema package, which declares its own narrow Connection interface and is imported BY this package's callers rather than by this package. A getter here would have to import schema, and schema would go on needing a connection: Go refuses the cycle, so the constructor is the call site instead. It takes the connection and the process factory.

A seeder is handed what it needs rather than reaching for it; see Seeder.

Index

Constants

View Source
const DefaultSQLitePath = "database/database.sqlite"

DefaultSQLitePath is where a fresh project keeps its database file.

View Source
const DefaultURL = "sqlite://" + DefaultSQLitePath

DefaultURL is what a project with no DATABASE_URL runs on: a file, no server, nothing installed.

View Source
const KeyText = "VARCHAR(255)"

KeyText is how a text column that takes part in a key is declared.

TEXT is the portable spelling for text, and it is the wrong one for anything indexed: MySQL stores TEXT off-page and refuses it in a key without a prefix length, so `id TEXT PRIMARY KEY` fails with "BLOB/TEXT column used in key specification without a key length". That is the first statement `aru migrate` runs, which means MySQL never got past creating the tracking table -- and every project table repeated the same mistake. Found by audit.

VARCHAR(255) is accepted by all three: PostgreSQL treats it as varchar, SQLite gives it TEXT affinity because the name contains CHAR, and MySQL indexes it. 255 rather than something tighter so there is one width to remember, and because two of them in a composite index still fit under InnoDB's key limit.

The rule: TEXT for free-form content nobody indexes, KeyText for an id, a tenant, or anything a UNIQUE or an index names.

Variables

View Source
var DefaultPostProcessor func(dialect Dialect) query.Processor

DefaultPostProcessor is where UseDefaultPostProcessor gets its processor.

It is keyed by dialect like DefaultQueryGrammar, because the processors differ by dialect: reading back the identifier of an inserted row is a returning clause on one engine and an out-of-band value on another.

View Source
var DefaultQueryGrammar func(dialect Dialect) query.Grammar

DefaultQueryGrammar is where UseDefaultQueryGrammar gets its grammar.

It holds the shipped grammar for the dialect, and a connector that speaks an engine this framework does not ship replaces it from its own init, next to the driver it registers. Assigning nil leaves the grammar unset, which a connection built for a test that never compiles SQL can do.

View Source
var ErrMultipleColumnsSelected = errors.New("database: more than one column was selected")

ErrMultipleColumnsSelected is what Scalar raises when the row it read has more than one column.

View Source
var ErrNotFound = errors.New("database: no such row")

ErrNotFound is what Find returns when the row is not there.

It is a sentinel rather than sql.ErrNoRows for two reasons. A repository that leaks sql.ErrNoRows makes every caller import database/sql to compare against it, and the exception classifier turns this one into a 404 -- so a module that returns it gets the right status without writing a status anywhere.

A repository wraps it (fmt.Errorf("%w: invoice %s", database.ErrNotFound, id)) so errors.Is keeps working and the message still says which row.

View Source
var ErrRecordNotFound = concerns.ErrRecordNotFound

ErrRecordNotFound is what FirstOrFail returns when the query matched no row.

It is the same value as concerns.ErrRecordNotFound, re-exported: FirstOrFail lives there, and errors.Is has to keep working across both names, which it does because there is only one.

View Source
var ErrRecordsNotFound = concerns.ErrRecordsNotFound

ErrRecordsNotFound is what Sole returns when the query matched no row at all.

It is the same value as concerns.ErrRecordsNotFound, re-exported for the same reason ErrRecordNotFound is: Sole lives in that package, and errors.Is has to give the same answer under either name.

View Source
var ErrSQLiteDatabaseDoesNotExist = errors.New("database file does not exist")

ErrSQLiteDatabaseDoesNotExist reports that the SQLite file named by the configuration is not there.

The path is wrapped into the message rather than carried in a field, because a caller that has the error has nothing to do with the path except print it.

View Source
var SchemaBuilderFactory func(*Connection) any

SchemaBuilderFactory is where the schema package registers how to build a schema builder for a connection.

Constructing one directly here would make this package import database/schema, while that package imports this one for the connection it builds against. The registration goes the other way and closes the cycle.

View Source
var UniqueConstraintDetector func(driver string, err error) bool

UniqueConstraintDetector says whether a driver error was a unique constraint violation. A connector sets it next to the driver it registers, because the answer is the driver's SQLSTATE and nothing this package can read.

Functions

func AfterCommit added in v0.27.0

func AfterCommit(ctx context.Context, db *DB, fn func(context.Context)) error

AfterCommit runs fn once the outermost transaction on db has committed.

It is how work that must not happen twice, and must not happen at all if the write did not, is attached to a write: a notification, a cache invalidation, a job handed to a queue. Registered at any depth, it belongs to the outermost transaction, because that is the one whose commit makes anything durable.

Outside a transaction it runs immediately, which is the same promise kept under the only circumstances there are: the write it is about has already happened.

A rollback discards it. So does a panic, because the deferred rollback is what runs. There is no callback for that case here -- a caller that needs to know a transaction failed reads the error it returned.

fn is handed a context on which InTransaction reports false, so a callback cannot mistake itself for part of the write and cannot open a statement that joins a transaction that has ended.

This is not durable delivery

A process that dies between the commit and the callback loses it, and no amount of ordering inside one process fixes that. What after-commit removes is the announcement of a write that was rolled back; what it does not add is a guarantee that the announcement arrives. Work that has to arrive is written into the same transaction as the row and read out of the database afterwards -- an outbox -- and deferring a callback is not that, however carefully it is deferred.

func AvailableDrivers

func AvailableDrivers() []string

AvailableDrivers returns the supported drivers that this binary can actually reach: the intersection of SupportedDrivers and the connector registry, which reports what main.go imported -- the same question asked of the thing that decides it here.

func CalculateDynamicConnectionName

func CalculateDynamicConnectionName(config map[string]any) string

CalculateDynamicConnectionName derives a stable name for an on-demand connection by hashing its configuration.

It concatenates key and value for every entry and hashes the result; map order is not stable, so the keys are sorted first, or the same configuration would get two different names on different runs.

func CausedByConcurrencyError

func CausedByConcurrencyError(err error) bool

CausedByConcurrencyError reports whether an error means a deadlock or a serialization failure.

It reads the package's own ConcurrencyErrorDetector; there is no detector to register and none to swap.

func CausedByLostConnection

func CausedByLostConnection(err error) bool

CausedByLostConnection reports whether an error means the connection is gone.

It reads the package's own LostConnectionDetector; there is no detector to register and none to swap.

func Day

func Day(t time.Time) time.Time

Day truncates a time to midnight UTC, which is what a date column means.

It exists because DATE is the one type in the portable subset that the three engines do not agree about. PostgreSQL drops the time part on write, so the value read back differs from the one written. SQLite gives DATE numeric affinity and stores whatever the driver sends, time and zone included -- so the same code, on the same day, returns different values depending on the engine, and a comparison between two dates is true on one and false on the other. Found by audit.

Normalizing on the way in makes the engines agree: what SQLite stores is what PostgreSQL would have stored anyway. `aru make:module` emits it for every field declared as a date.

func DriverName

func DriverName(d Dialect) (string, error)

DriverName returns the database/sql driver a compartment registered for a dialect, or the error that names the missing import.

It exists for the conformance suite, which has to open a connection with the same driver Open would use rather than a name it hardcoded -- a suite testing a driver nobody links is a suite testing nothing.

func DriverTitle

func DriverTitle(driver string) string

DriverTitle answers the human name of a driver, and the driver itself for one nobody named.

func Flag

func Flag(args []string, name string) (string, bool)

Flag reads `-name value` out of the arguments.

A hand-written reader rather than the flag package, because the flag package wants to own os.Args, prints its own usage to stderr and calls os.Exit on a mistake -- in the middle of a command that has already opened a database.

It accepts `-name value` and `-name=value`. A long form (`--name`) is the same flag: nobody should have to remember how many dashes a seeder wanted.

func ForMigrations

func ForMigrations(connection *Connection) migrations.Connection

ForMigrations adapts a Connection to migrations.Connection.

The migrations package declares its own narrow connection interface, because it cannot import this one -- this one resolves migrations. The two signatures differ in exactly two places, and both differences are the reason this adapter is four methods rather than a type assertion:

  • Select there takes no read-replica flag, and always reads through the write pool. A migration that read from a replica would be reading a schema the replica has not been told about yet.
  • Pretend there returns the statements as strings, because the migrator prints them and has no use for the bindings.

The result also satisfies migrations.TransactionalConnection and migrations.PretendingConnection, so `aru migrate` gets its transaction wrapping and its --pretend from the same value.

func ForSchema added in v0.15.0

func ForSchema(connection *Connection) schema.Connection

ForSchema adapts a Connection to schema.Connection.

It is the seam between the connection this package owns and the schema package, and it exists rather than the concrete Connection growing the seventeen methods of that interface. Two reasons, and the second is the one that decides:

The signatures do not line up. schema.Connection wants Select(ctx, query) and this connection answers Select(ctx, query, bindings, useReadPDO); it wants GetServerVersion() string and this one answers (string, error) and takes a context; it wants GetConfig(option) string where this one answers any. Every one of those is a wrapper, and a wrapper on the concrete type would be a second spelling of a method that already exists.

And an adapter keeps the change local. Making Connection implement schema.Connection would put six processors, a grammar choice and a version lookup on the type every repository, every migration and the Collector reach through -- which is the type least able to absorb an unrelated change.

No Grant

Nothing here takes one. DDL names a table, not a row: there is no tenant to scope by, no subject to attribute to, and no request it came from. The path to application rows is the one that needs a Grant, and it still has one on every method.

func GetResolver

func GetResolver(driver string) func(pdo *sql.DB, database, prefix string, config map[string]any) *Connection

GetResolver returns the constructor ResolverFor registered for driver, or nil when none was.

func InTransaction

func InTransaction(ctx context.Context, db *DB) bool

InTransaction reports whether the context is inside a transaction on db.

The outbox uses it to refuse to store an event outside a transaction, which is the whole guarantee: an event written next to a row that rolled back is worse than no event at all.

It takes the handle because "in a transaction" is only meaningful about one database. An outbox on the analytics handle is not protected by a transaction open on the primary.

func NewID

func NewID() (string, error)

NewID returns a version 4 UUID as text.

Ids are generated by the application, not by the database: gen_random_uuid, UUID() and randomblob are three different spellings of the same idea, and depending on any of them would tie the schema to one engine.

func NewSQLiteDatabaseDoesNotExistException

func NewSQLiteDatabaseDoesNotExistException(path string) error

NewSQLiteDatabaseDoesNotExistException answers its constructor.

func Register

func Register(c Connector)

Register records that a connector for this dialect is linked into the binary.

Connectors call it from init(). It is not meant for application code: a project that registers its own driver name is a project that will get a different pool policy than every other, for no gain.

Registering the same dialect twice panics rather than picking one. Two drivers for one dialect is an import nobody meant to add, and finding out at boot is better than finding out from a query that behaves differently.

func ResolverFor

func ResolverFor(driver string, callback func(pdo *sql.DB, database, prefix string, config map[string]any) *Connection)

ResolverFor registers how a driver builds its connection.

It is what lets a connector supply a driver-specific Connection without this package importing it, and what lets a project add a driver this framework does not ship.

func Seed

func Seed[D any](ctx context.Context, registry []Seeder[D], fallback string, args []string, deps func(rest []string) D) (string, error)

Seed runs the seeder named in args, or fallback when none is, and returns the name of the one that ran.

aru db:seed                                    the fallback
aru db:seed PostSeeder                         one of them
aru db:seed UserSeeder -e a@b.com -p secret    one of them, with arguments

The name is positional. A name that is sometimes a flag and sometimes a word is two spellings of one thing, so the `--class=` form is refused with the word to use instead rather than accepted quietly.

Everything after the name reaches deps, unparsed, which is why deps is a function rather than a value: the project's Deps carries an Args field and this package does not know the field exists. What a flag means is the seeder's business -- this function does not know that UserSeeder has a -p, and adding a seeder does not edit this file. Flag and Switch below are what a seeder reads them with.

It prints nothing. A library that writes to stdout is a library that cannot be called from a test, and the caller already knows which name to report.

func SupportedDrivers

func SupportedDrivers() []string

SupportedDrivers answers the dialects this package can open.

SQL Server is not on it and will not be, and mariadb is spoken by the MySQL connector rather than a fourth one.

func Switch

func Switch(args []string, name string) bool

Switch reports whether a valueless flag is present.

func Transaction

func Transaction(ctx context.Context, db *DB, fn func(context.Context) error) error

Transaction runs fn inside a database transaction.

Every statement issued through the same *DB while fn runs joins it, because the transaction travels on the context. Returning an error rolls back; returning nil commits. A panic rolls back and keeps panicking -- swallowing it would leave the caller believing the write happened.

A Transaction inside a Transaction joins the outer one rather than opening a second. There are no savepoints: partial rollback is a second failure mode for the same operation, and the shape this framework wants is one write, one outcome.

func TransactionAt added in v0.26.0

func TransactionAt(ctx context.Context, db *DB, level sql.IsolationLevel, fn func(context.Context) error) error

TransactionAt runs fn inside a transaction opened at the named isolation level.

It is Transaction with the level stated instead of inherited, and everything said there holds here: the transaction travels on the context, returning an error rolls back, a panic keeps panicking, and a transaction inside a transaction joins the outer one.

Why the level is named when the transaction opens

The alternative is a SET statement as the first thing inside it, and that alternative is not portable. PostgreSQL takes SET TRANSACTION ISOLATION LEVEL inside an open transaction; MySQL refuses it there -- the characteristics of a transaction cannot be changed once it is in progress -- and would need SET SESSION instead, which outlives the transaction and rides the connection back into the pool set for everybody. Naming it here hands the level to BeginTx, where the driver of each engine spells it the way that engine takes.

A joined transaction keeps the level it was opened at

Where the context already carries a transaction on this handle, fn joins it and the level asked for here is not applied. Changing the level of a transaction somebody else opened would be deciding about statements this call cannot see, and on MySQL it is not expressible at all. A caller that needs a guarantee from the level opens the transaction itself.

sql.LevelDefault is what Transaction passes, and it means the engine's own default -- which is not the same on every engine, and is a setting an operator can change for a whole cluster. Code whose correctness rests on what a predicate is evaluated against while another transaction changes the same row names the level rather than inheriting it.

Types

type ConcurrencyErrorDetector

type ConcurrencyErrorDetector struct{}

ConcurrencyErrorDetector reads a driver error and says whether it was a deadlock or a serialization failure.

func NewConcurrencyErrorDetector

func NewConcurrencyErrorDetector() *ConcurrencyErrorDetector

NewConcurrencyErrorDetector creates a ConcurrencyErrorDetector.

func (*ConcurrencyErrorDetector) CausedByConcurrencyError

func (d *ConcurrencyErrorDetector) CausedByConcurrencyError(err error) bool

CausedByConcurrencyError reports whether err indicates a deadlock or a serialization failure.

database/sql carries no error code, and 40001 is the SQLSTATE for a serialization failure, so the check looks for that string in the message text -- which is where pgx and go-sql-driver both put it.

type Config

type Config struct {
	Connection Dialect
	Database   string // file path for SQLite, database name otherwise
	Host       string
	Port       string
	Username   string
	Password   string

	// Options is the query string, carried through to the driver. sslmode for
	// Postgres, parseTime for MySQL, the _pragma list for SQLite -- anything
	// the driver understands and this package should not have an opinion about.
	Options url.Values

	// URL is what was parsed, kept for the error messages and for a driver that
	// would rather have the string than the parts.
	URL string

	// MaxOpenConns caps the connections in flight. Zero is 25.
	//
	// Zero on this field and the two below means the default of this package,
	// and never database/sql's meaning for zero, which is unbounded. That
	// difference is the whole rule: ParseURL leaves all three at zero, so every
	// configuration built from a URL -- which is every configuration there is --
	// carries zero. Reading it as database/sql does would take the bound off
	// every pool at once, and the bound is the reason [Open] sets these at all:
	// an unbounded pool turns one traffic spike into "too many connections" on
	// the server instead of a queue in this process.
	//
	// There is therefore no way to ask for an unbounded pool, deliberately. The
	// answer to a pool that is too small is a larger number, and the answer to
	// one that is too small at every number is a queue.
	//
	// SQLite ignores this one: it gets a single writer whatever is set here.
	// See [Open].
	MaxOpenConns int

	// MaxIdleConns is how many connections stay open between queries. Zero is 5.
	//
	// database/sql caps it at MaxOpenConns on its own, so a number above the
	// open limit is the open limit.
	MaxIdleConns int

	// ConnMaxLifetime retires a connection after this long. Zero is one hour.
	//
	// A managed database that rotates behind a proxy hands out connections that
	// stop working, and a lifetime is what makes the pool replace them without a
	// request failing first.
	ConnMaxLifetime time.Duration
}

Config describes one connection.

It comes from ONE variable, and that is the whole of the design:

DATABASE_URL=postgres://arandu:arandu@127.0.0.1:5432/arandu?sslmode=disable
DATABASE_URL=mysql://arandu:arandu@127.0.0.1:3306/arandu
DATABASE_URL=sqlite://database/database.sqlite

There is no second set of variables -- no DB_HOST, no DB_PORT, no DB_DATABASE -- because two ways to say where the database is produces the worst kind of failure: half the parts overridden and half not, an application pointing at a host from one variable and a database from another, with every value individually correct.

One string also travels. Every managed platform hands out exactly this, a compose file passes it as one line, and a person moving between them copies one thing instead of six.

It lives here rather than in hesape/config because the reader of DATABASE_URL and the opener of the connection are one decision, and splitting them puts an import from the configuration package into the SQL package, pointing the wrong way.

The connection fields below are what the URL parsed into. They are read, never written: what wrote them is ParseURL, in one place, with one set of rules. The three pool fields are the exception and are documented as such -- ParseURL never touches them, because how many connections to hold is not part of where the database is.

func Load

func Load() (Config, error)

Load reads the connection out of the environment.

It is the whole of the database's configuration: one variable, parsed by ParseURL, with the retired DB_* block refused rather than ignored.

func ParseURL

func ParseURL(raw string) (Config, error)

ParseURL reads the one variable.

The schemes are the conventional ones, so a string copied from a platform, a compose file or another language works unchanged: postgres, postgresql, mysql, sqlite, file.

func (Config) DSN

func (d Config) DSN() string

DSN returns the connection string for the driver of this dialect.

func (Config) Redacted

func (d Config) Redacted() string

Redacted returns the connection as a string safe to log or show on the error page: the password is never part of it.

func (Config) SQLitePath

func (d Config) SQLitePath() string

SQLitePath returns the file the database lives in, or the empty string for a server-based connection. The application uses it to create the directory before opening: SQLite creates the file, never the directory above it.

func (Config) Validate

func (d Config) Validate() error

Validate reports a connection that cannot work, at boot rather than on the first query.

type Configuration

type Configuration interface {
	// Get returns the configuration value for key.
	Get(key string) any

	// Set replaces the configuration value for key.
	Set(key string, value any)
}

Configuration is where DatabaseManager reads its connections: the two keys 'database.default' and 'database.connections'.

It is an interface rather than hesape/config directly because the manager has to be constructible in a test with three lines and no file.

type ConfigurationUrlParser

type ConfigurationUrlParser = support.ConfigurationUrlParser

ConfigurationUrlParser reads a database URL into its parts.

It is an alias of support.ConfigurationUrlParser, so a caller in this package can name it without importing that one. There is exactly one implementation.

func NewConfigurationUrlParser

func NewConfigurationUrlParser() *ConfigurationUrlParser

NewConfigurationUrlParser creates a ConfigurationUrlParser.

type Connection

type Connection struct {
	// ManagesTransactions embeds the concerns.ManagesTransactions
	// implementation: Transaction, BeginTransaction, Commit, RollBack,
	// TransactionLevel, AfterCommit and AfterRollBack are all reached through
	// it.
	concerns.ManagesTransactions
	// contains filtered or unexported fields
}

Connection is one open connection, and everything that runs statements through it.

Where the Grant is, and why it is not on these methods

The door application code goes through is Repository, one file over, whose every method takes an auth.Grant and filters by auth.Tenant(g) -- on the way out as much as on the way in.

A Connection is the plumbing underneath that door, and it takes no Grant on purpose. It could not use one: a Grant is enforced by filtering a query by tenant, and this type is handed a finished string. A parameter that looks like enforcement and enforces nothing is worse than no parameter, because the next reader stops checking -- the same reasoning that removed Query.Filter. The other half of the argument is that `aru migrate` runs here, in a process with no request and no subject, where a Grant cannot be constructed at all.

So what keeps rows behind a Policy at this level is convention, not the type system: Select, Statement, Insert and the rest are exported, take no Grant, and reach the same rows a Repository does. Reach them through a Repository. Reaching them through a Connection is how a module gets rejected in review.

A connection is a pool

The write handle and the read handle are both *sql.DB, which is a pool rather than a socket -- so "reconnect" means replacing the pool, and the read/write split is two pools rather than two connections.

func NewConnection

func NewConnection(pdo *sql.DB, database, tablePrefix string, config map[string]any) *Connection

NewConnection creates a Connection over an already-open pool.

A nil pool is valid: it stands for a connection that has not been opened yet, and Reconnect is what fills it in.

func (*Connection) AffectingStatement

func (c *Connection) AffectingStatement(ctx context.Context, q string, bindings []any) (int64, error)

AffectingStatement runs a statement and returns the number of rows it affected.

func (*Connection) AllowQueryDurationHandlersToRunAgain

func (c *Connection) AllowQueryDurationHandlersToRunAgain()

AllowQueryDurationHandlersToRunAgain resets every WhenQueryingForLongerThan handler so it can fire again.

func (*Connection) BeforeExecuting

func (c *Connection) BeforeExecuting(callback func(query string, bindings []any, connection *Connection)) *Connection

BeforeExecuting registers a callback that runs before every statement.

func (*Connection) BindValues

func (c *Connection) BindValues(bindings []any) []any

BindValues is PrepareBindings under another name: database/sql infers each value's wire type from its Go type, so there is no separate type-binding step to do -- an int is an int, a byte slice is a blob, and everything else is a string as far as the driver is concerned.

func (*Connection) CausedByConcurrencyError

func (c *Connection) CausedByConcurrencyError(err error) bool

CausedByConcurrencyError reports whether err means a deadlock or a serialization failure, using the package-level detector.

func (*Connection) CausedByLostConnection

func (c *Connection) CausedByLostConnection(err error) bool

CausedByLostConnection reports whether err means the connection is gone, using the package-level detector.

func (*Connection) CommitTransactionStatement

func (c *Connection) CommitTransactionStatement() error

CommitTransactionStatement issues a COMMIT on the pinned transaction connection, and releases it back to the pool.

func (*Connection) CompileSavepoint

func (c *Connection) CompileSavepoint(name string) string

CompileSavepoint returns the statement that creates savepoint name, using the query grammar, or the base grammar when none is set.

func (*Connection) CompileSavepointRollBack

func (c *Connection) CompileSavepointRollBack(name string) string

CompileSavepointRollBack returns the statement that rolls back to savepoint name, using the query grammar, or the base grammar when none is set.

func (*Connection) Cursor

func (c *Connection) Cursor(ctx context.Context, q string, bindings []any, useReadPDO bool) func(yield func(query.Record, error) bool)

Cursor runs a query and returns the rows one at a time, without holding the whole result set in memory.

It returns a range-over-func iterator with the same shape as concerns.Lazy: an error is yielded once and ends the iteration, so a caller that forgets to check it still stops.

It does not go through run, because run wraps a callback that has to finish before it returns and this one hands rows back as they arrive. The renumbering is therefore repeated here rather than inherited, and it is the only place in this file where that is true.

func (*Connection) Delete

func (c *Connection) Delete(ctx context.Context, q string, bindings []any) (int64, error)

Delete runs a delete statement and returns the number of rows it removed.

func (*Connection) DisableQueryLog

func (c *Connection) DisableQueryLog()

DisableQueryLog turns query logging off.

func (*Connection) Disconnect

func (c *Connection) Disconnect()

Disconnect clears both the write and the read pool.

func (*Connection) EnableQueryLog

func (c *Connection) EnableQueryLog()

EnableQueryLog turns query logging on.

func (*Connection) Escape

func (c *Connection) Escape(value any, binary bool) (string, error)

Escape renders value as a SQL literal.

It is the one method in this file that should almost never be called: a value that goes through here did not go through a placeholder. The grammar uses it to write a literal into a schema dump, which is the case it exists for.

func (*Connection) ExecuteBeginTransactionStatement

func (c *Connection) ExecuteBeginTransactionStatement() error

ExecuteBeginTransactionStatement issues a BEGIN on a dedicated connection taken from the pool, and holds that connection for the life of the transaction.

database/sql has no BEGIN outside sql.Tx, so this spells out what BeginTx does internally: ManagesTransactions' bookkeeping is what decides when the matching COMMIT runs.

func (*Connection) ExecuteSavepointStatement

func (c *Connection) ExecuteSavepointStatement(statement string) error

ExecuteSavepointStatement runs a savepoint statement on the open transaction.

func (*Connection) FireConnectionEvent

func (c *Connection) FireConnectionEvent(event string)

FireConnectionEvent dispatches the transaction event named by event: one of "beganTransaction", "committed", "committing" or "rollingBack".

It is exported because ManagesTransactions calls it from the concerns package.

func (*Connection) FlushQueryLog

func (c *Connection) FlushQueryLog()

FlushQueryLog clears the query log.

func (*Connection) ForgetRecordModificationState

func (c *Connection) ForgetRecordModificationState()

ForgetRecordModificationState clears the modified flag.

func (*Connection) GetConfig

func (c *Connection) GetConfig(option string) any

GetConfig returns one configuration value by key. An empty key returns nothing rather than the whole configuration, because a caller that wants all of it can ask for the keys it needs.

func (*Connection) GetConnectionDetails

func (c *Connection) GetConnectionDetails() map[string]any

GetConnectionDetails returns the driver, name, host, port, database and socket this connection reports, reading from the read configuration when a read pool was last used. It is exported because QueryException carries it.

func (*Connection) GetDatabaseName

func (c *Connection) GetDatabaseName() string

GetDatabaseName returns the name of the database this connection is open on.

func (*Connection) GetDriverName

func (c *Connection) GetDriverName() string

GetDriverName returns the configured driver name.

func (*Connection) GetDriverTitle

func (c *Connection) GetDriverTitle() string

GetDriverTitle returns the human-facing name of the driver, which by default is just the driver name.

func (*Connection) GetEventDispatcher

func (c *Connection) GetEventDispatcher() Dispatcher

GetEventDispatcher returns the dispatcher this connection fires events on.

func (*Connection) GetName

func (c *Connection) GetName() string

GetName returns the connection's configured name.

func (*Connection) GetNameWithReadWriteType

func (c *Connection) GetNameWithReadWriteType() string

GetNameWithReadWriteType returns the connection's name, suffixed with "::read" or "::write" when a read/write type is set.

func (*Connection) GetPDO

func (c *Connection) GetPDO() (*sql.DB, error)

GetPDO returns the write pool, or an error when the connection has none.

func (*Connection) GetPostProcessor

func (c *Connection) GetPostProcessor() query.Processor

GetPostProcessor returns the processor this connection post-processes query results with.

func (*Connection) GetQueryGrammar

func (c *Connection) GetQueryGrammar() query.Grammar

GetQueryGrammar returns the grammar this connection compiles queries with.

func (*Connection) GetQueryLog

func (c *Connection) GetQueryLog() []QueryLogEntry

GetQueryLog returns a copy of the queries logged so far.

func (*Connection) GetRawPDO

func (c *Connection) GetRawPDO() *sql.DB

GetRawPDO returns the write pool as it is, nil included, with no attempt to reconnect.

func (*Connection) GetRawQueryLog

func (c *Connection) GetRawQueryLog() []QueryLogEntry

GetRawQueryLog returns the query log with each statement's bindings written directly into it.

func (*Connection) GetRawReadPDO

func (c *Connection) GetRawReadPDO() *sql.DB

GetRawReadPDO returns the read pool as it is, nil included.

func (*Connection) GetReadPDO

func (c *Connection) GetReadPDO() (*sql.DB, error)

GetReadPDO returns the read pool, or the write pool when it is more appropriate.

Three conditions send a read to the write pool instead: an open transaction, an explicit UseWriteConnectionWhenReading, and a sticky connection that has already written. The last is what keeps a read-after-write in one request from landing on a replica that has not caught up.

func (*Connection) GetSchemaBuilder

func (c *Connection) GetSchemaBuilder() any

GetSchemaBuilder returns the schema builder for this connection, built through SchemaBuilderFactory.

It returns any rather than a schema builder type for the reason above. Nil means the binary never imported the schema package, which is the truthful response to "give me the schema builder I did not link".

func (*Connection) GetSchemaGrammar

func (c *Connection) GetSchemaGrammar() any

GetSchemaGrammar returns the schema grammar this connection holds.

It is any because the schema grammars live in database/schema/grammars, which nothing here should import: a connection needs to hold one and never to call it, and the schema builder that does call it knows the type.

func (*Connection) GetServerVersion

func (c *Connection) GetServerVersion(ctx context.Context) (string, error)

GetServerVersion asks the server for its version string.

database/sql has no driver-level way to read it, so it is read with a query: version() is spelled the same by PostgreSQL and MySQL, and SQLite's sqlite_version() is substituted for it.

func (*Connection) GetTablePrefix

func (c *Connection) GetTablePrefix() string

GetTablePrefix returns the prefix prepended to every table name.

func (*Connection) HasModifiedRecords

func (c *Connection) HasModifiedRecords() bool

HasModifiedRecords reports whether a write has gone through this connection.

func (*Connection) Insert

func (c *Connection) Insert(ctx context.Context, q string, bindings []any) (bool, error)

Insert runs an insert statement, reporting whether it succeeded.

func (*Connection) InsertReturningID

func (c *Connection) InsertReturningID(ctx context.Context, q string, bindings []any) (int64, error)

InsertReturningID runs an insert and returns the identifier the engine assigned to the row.

The statement that caused the identifier already reports it, through sql.Result.LastInsertId, with no round trip needed to ask for it separately. Asking a second time would be a second way and, on a pooled connection, a way that can report somebody else's row.

Postgres does not travel this path at all: its processor compiles the insert with a RETURNING clause and reads the identifier out of the result set, which is why PostgresProcessor.ProcessInsertGetID calls Select rather than this.

A driver that cannot report one -- lib/pq is the usual case -- returns an error from LastInsertId, and it is passed through rather than flattened to a zero, because a zero identifier reads like a row.

func (*Connection) IsUniqueConstraintError

func (c *Connection) IsUniqueConstraintError(err error) bool

IsUniqueConstraintError reports whether err came from a unique constraint violation, using the UniqueConstraintDetector a connector registered; it is false when none was.

It is exported so a driver connection living in another package can call it -- a connector sets UniqueConstraintDetector rather than overriding a method, which is the same decision as DefaultQueryGrammar.

func (*Connection) Listen

func (c *Connection) Listen(callback func(*dbevents.QueryExecuted))

Listen registers callback to run for every query.

The connection keeps its own list of listeners and calls each one alongside dispatching the QueryExecuted event, which is what a listener registered on the event dispatcher directly would receive anyway.

func (*Connection) LogQuery

func (c *Connection) LogQuery(q string, bindings []any, timeMS float64)

LogQuery records a query's execution: it dispatches the QueryExecuted event, runs the query listeners and duration handlers, and appends to the query log when logging is enabled.

func (*Connection) Logging

func (c *Connection) Logging() bool

Logging reports whether query logging is on.

func (*Connection) PrepareBindings

func (c *Connection) PrepareBindings(bindings []any) []any

PrepareBindings converts each binding into the value a driver accepts.

A time becomes the string the grammar's date format spells, and a bool becomes 0 or 1 -- both because the engines disagree about the wire form and the grammar is the one that knows which. A value that spells its own column is asked for that spelling first, and what it answers is converted the same way anything else is.

func (*Connection) Pretend

func (c *Connection) Pretend(ctx context.Context, callback func(*Connection) error) ([]QueryLogEntry, error)

Pretend runs callback with nothing reaching the server, and returns the statements it would have run.

func (*Connection) Pretending

func (c *Connection) Pretending() bool

Pretending reports whether the connection is inside a Pretend call.

func (*Connection) Query

func (c *Connection) Query() *query.Builder

Query returns a fresh query builder on this connection.

func (*Connection) Raw

func (c *Connection) Raw(value any) query.Expression

Raw wraps value as a fragment of SQL that the grammar leaves untouched.

func (*Connection) Reconnect

func (c *Connection) Reconnect() error

Reconnect replaces the pool by calling the registered reconnector, or fails when none was set.

func (*Connection) ReconnectIfMissingConnection

func (c *Connection) ReconnectIfMissingConnection() error

ReconnectIfMissingConnection reconnects when the connection has no pool yet, and does nothing otherwise.

func (*Connection) RecordsHaveBeenModified

func (c *Connection) RecordsHaveBeenModified(value bool)

RecordsHaveBeenModified sets the modified flag, but only from false to true: once a write has happened, nothing clears it but ForgetRecordModificationState.

func (*Connection) ResetTotalQueryDuration

func (c *Connection) ResetTotalQueryDuration()

ResetTotalQueryDuration sets the total query duration back to zero.

func (*Connection) RollBackTransactionStatement

func (c *Connection) RollBackTransactionStatement() (bool, error)

RollBackTransactionStatement issues a ROLLBACK on the pinned transaction connection and releases it, reporting false when there was no transaction connection to roll back.

func (*Connection) Scalar

func (c *Connection) Scalar(ctx context.Context, q string, bindings []any, useReadPDO bool) (any, error)

Scalar runs a query and returns the first column of the first row.

More than one column selected is an error: a query that returns two columns and is read as one is a query somebody edited without reading its caller.

func (*Connection) Select

func (c *Connection) Select(ctx context.Context, q string, bindings []any, useReadPDO bool) ([]query.Record, error)

Select runs a query and returns every matching row.

func (*Connection) SelectFromWriteConnection

func (c *Connection) SelectFromWriteConnection(ctx context.Context, q string, bindings []any) ([]query.Record, error)

SelectFromWriteConnection runs a read against the write pool, for a read that must see writes this request already made.

func (*Connection) SelectOne

func (c *Connection) SelectOne(ctx context.Context, q string, bindings []any, useReadPDO bool) (query.Record, bool, error)

SelectOne runs a query and returns the first row, or false when there was none.

A nil record is indistinguishable from a row of no columns, so the second value says which it was.

func (*Connection) SelectResultSets

func (c *Connection) SelectResultSets(ctx context.Context, q string, bindings []any, useReadPDO bool) ([][]query.Record, error)

SelectResultSets runs a statement and returns every result set it produced, not only the first.

database/sql exposes the extra sets through Rows.NextResultSet. A driver that does not support it returns exactly one set.

func (*Connection) SetDatabaseName

func (c *Connection) SetDatabaseName(database string) *Connection

SetDatabaseName replaces the name of the database this connection is open on.

func (*Connection) SetEventDispatcher

func (c *Connection) SetEventDispatcher(dispatcher Dispatcher) *Connection

SetEventDispatcher replaces the dispatcher this connection fires events on.

func (*Connection) SetPDO

func (c *Connection) SetPDO(pdo *sql.DB) *Connection

SetPDO replaces the write pool. It resets the transaction level, because whatever transaction was open belonged to the pool being replaced.

func (*Connection) SetPostProcessor

func (c *Connection) SetPostProcessor(processor query.Processor) *Connection

SetPostProcessor replaces the processor this connection post-processes query results with.

func (*Connection) SetQueryGrammar

func (c *Connection) SetQueryGrammar(grammar query.Grammar) *Connection

SetQueryGrammar replaces the grammar this connection compiles queries with.

func (*Connection) SetReadPDO

func (c *Connection) SetReadPDO(pdo *sql.DB) *Connection

SetReadPDO replaces the read pool.

func (*Connection) SetReadPDOConfig

func (c *Connection) SetReadPDOConfig(config map[string]any) *Connection

SetReadPDOConfig replaces the configuration reported for the read pool.

func (*Connection) SetReadWriteType

func (c *Connection) SetReadWriteType(readWriteType string) *Connection

SetReadWriteType sets the read/write suffix GetNameWithReadWriteType reports.

func (*Connection) SetReconnector

func (c *Connection) SetReconnector(reconnector func(*Connection) error) *Connection

SetReconnector sets the callback Reconnect calls to replace the pool.

func (*Connection) SetRecordModificationState

func (c *Connection) SetRecordModificationState(value bool) *Connection

SetRecordModificationState sets the modified flag directly, unlike RecordsHaveBeenModified, which only ever sets it to true.

func (*Connection) SetSchemaGrammar

func (c *Connection) SetSchemaGrammar(grammar any) *Connection

SetSchemaGrammar replaces the schema grammar this connection holds.

func (*Connection) SetTablePrefix

func (c *Connection) SetTablePrefix(prefix string) *Connection

SetTablePrefix replaces the prefix prepended to every table name.

func (*Connection) Statement

func (c *Connection) Statement(ctx context.Context, q string, bindings []any) (bool, error)

Statement runs a statement that returns neither rows nor an affected-row count, reporting whether it succeeded.

func (*Connection) SubstituteBindingsIntoRawSQL

func (c *Connection) SubstituteBindingsIntoRawSQL(sql string, bindings []any) string

SubstituteBindingsIntoRawSQL writes bindings directly into sql, so a Connection satisfies events.RawSQLConnection.

func (*Connection) SupportsSavepoints

func (c *Connection) SupportsSavepoints() bool

SupportsSavepoints reports whether the query grammar supports savepoints, falling back to the base grammar when none is set.

func (*Connection) Table

func (c *Connection) Table(table any, as ...string) *query.Builder

Table returns a query builder against one table, with an optional alias.

It takes no context, and it used to. The context belongs to the statement, not to the builder: every terminal method takes one, and a builder is a value a caller may hold across more than one of them.

func (*Connection) ThreadCount

func (c *Connection) ThreadCount(ctx context.Context) (int64, error)

ThreadCount returns how many connections the server has open, or zero when the grammar cannot ask.

func (*Connection) TotalQueryDuration

func (c *Connection) TotalQueryDuration() float64

TotalQueryDuration returns the total time spent on queries so far, in milliseconds.

func (*Connection) Unprepared

func (c *Connection) Unprepared(ctx context.Context, q string) (bool, error)

Unprepared runs the statement as it is, with no prepare and no bindings, reporting whether it succeeded.

It is what a schema dump is loaded with, and nothing else should use it: a value that reaches a database through this has not been through a placeholder.

func (*Connection) UnsetEventDispatcher

func (c *Connection) UnsetEventDispatcher()

UnsetEventDispatcher clears the event dispatcher, so events stop firing.

func (*Connection) Update

func (c *Connection) Update(ctx context.Context, q string, bindings []any) (int64, error)

Update runs an update statement and returns the number of rows it changed.

func (*Connection) UseDefaultPostProcessor

func (c *Connection) UseDefaultPostProcessor()

UseDefaultPostProcessor sets the connection's post-processor from DefaultPostProcessor, if one was registered.

func (*Connection) UseDefaultQueryGrammar

func (c *Connection) UseDefaultQueryGrammar()

UseDefaultQueryGrammar sets the connection's query grammar from DefaultQueryGrammar, if one was registered.

func (*Connection) UseDefaultSchemaGrammar

func (c *Connection) UseDefaultSchemaGrammar()

UseDefaultSchemaGrammar does nothing: a connection with no driver-specific override has no default schema grammar to set.

func (*Connection) UseWriteConnectionWhenReading

func (c *Connection) UseWriteConnectionWhenReading(value bool) *Connection

UseWriteConnectionWhenReading sets whether a read should go to the write pool instead of the read pool.

func (*Connection) WhenQueryingForLongerThan

func (c *Connection) WhenQueryingForLongerThan(threshold time.Duration, handler func(*Connection, *dbevents.QueryExecuted))

WhenQueryingForLongerThan runs the handler once the connection has spent more than the threshold on queries.

The threshold is a time.Duration, which is the one type Go has for a duration.

func (*Connection) WithoutPretending

func (c *Connection) WithoutPretending(callback func() error) error

WithoutPretending runs callback for real, even inside a Pretend.

func (*Connection) WithoutTablePrefix

func (c *Connection) WithoutTablePrefix(callback func(*Connection) error) error

WithoutTablePrefix runs callback with the table prefix cleared, restoring it afterward.

type ConnectionFactory

type ConnectionFactory struct{}

ConnectionFactory turns a configuration into an open connection, picking the driver by name.

It lives in the root package rather than in database/connectors, and that is the whole point of it. The connectors are separate Go modules -- pgx, mysql and sqlite each with their own go.mod -- because Go has no optional dependency, and a factory that imported all three to choose between them would put all three in the go.sum of every project. So the choice is made by registration instead:

import (
    "github.com/arandu-io/hesape/database"
    _ "github.com/arandu-io/hesape/database/connectors/pgx"
)

The blank import runs the connector's init, which calls database.Register with the dialect it supports and the database/sql driver name it linked. The factory reads that registry, and never names a driver package.

How a project registers its own

A project that speaks an engine this framework does not ship writes the same four lines the shipped connectors write:

package clickhouse

import (
    "github.com/arandu-io/hesape/database"
    _ "github.com/ClickHouse/clickhouse-go/v2"
)

type Connector struct{}

func (Connector) Dialect() database.Dialect { return "clickhouse" }
func (Connector) DriverName() string        { return "clickhouse" }

func init() { database.Register(Connector{}) }

and blank-imports it from main. Nothing in this package changes, and nothing in this package learns the name. If the connection also needs its own SQL spelling, it registers a grammar through DefaultQueryGrammar and a connection through ResolverFor, which are the same inversion one layer down.

func NewConnectionFactory

func NewConnectionFactory() *ConnectionFactory

NewConnectionFactory builds a ConnectionFactory. It takes nothing: what it needs is the driver registry, which is package state.

func (*ConnectionFactory) CreateConnection

func (f *ConnectionFactory) CreateConnection(driver string, pool *sql.DB, database, prefix string, config map[string]any) (*Connection, error)

CreateConnection returns the Connection for a driver, through the resolver a connector registered, or the plain constructor otherwise.

It is exported because a connector in another module registers through ResolverFor and a project building a connection by hand has no other door.

func (*ConnectionFactory) CreateConnector

func (f *ConnectionFactory) CreateConnector(config map[string]any) (Connector, error)

CreateConnector returns the connector registered for this configuration's driver.

The match is a registry lookup rather than a name comparison against every known driver, because the alternative is importing every driver package just to be able to name them.

func (*ConnectionFactory) Make

func (f *ConnectionFactory) Make(config map[string]any, name string) (*Connection, error)

Make opens one connection, or a read/write pair when the configuration has a "read" key.

type ConnectionInterface

type ConnectionInterface interface {
	// Table returns a query builder against one table, with an optional
	// alias.
	Table(table any, as ...string) *query.Builder

	// Query returns a fresh query builder bound to this connection. Table is
	// unusable without it, and there is no other route to a builder.
	Query() *query.Builder

	// Raw wraps value as a fragment of SQL that the grammar leaves
	// untouched.
	Raw(value any) query.Expression

	// SelectOne runs a query and returns the first row, or false when there
	// was none.
	SelectOne(ctx context.Context, query string, bindings []any, useReadPDO bool) (query.Record, bool, error)

	// Scalar runs a query and returns the first column of the first row.
	Scalar(ctx context.Context, query string, bindings []any, useReadPDO bool) (any, error)

	// Select runs a query and returns every matching row.
	Select(ctx context.Context, query string, bindings []any, useReadPDO bool) ([]query.Record, error)

	// Cursor runs a query and returns a range-over-func iterator that
	// yields the rows one at a time, without holding the whole result set
	// in memory.
	Cursor(ctx context.Context, query string, bindings []any, useReadPDO bool) func(yield func(query.Record, error) bool)

	// Insert runs an insert statement, reporting whether it succeeded.
	Insert(ctx context.Context, query string, bindings []any) (bool, error)

	// Update runs an update statement and returns the number of rows it
	// changed.
	Update(ctx context.Context, query string, bindings []any) (int64, error)

	// Delete runs a delete statement and returns the number of rows it
	// removed.
	Delete(ctx context.Context, query string, bindings []any) (int64, error)

	// Statement runs a statement that returns neither rows nor an
	// affected-row count, reporting whether it succeeded.
	Statement(ctx context.Context, query string, bindings []any) (bool, error)

	// AffectingStatement runs a statement and returns the number of rows it
	// affected.
	AffectingStatement(ctx context.Context, query string, bindings []any) (int64, error)

	// Unprepared runs a statement as it is, with no bindings to prepare.
	Unprepared(ctx context.Context, query string) (bool, error)

	// PrepareBindings converts each binding into the value a driver
	// accepts.
	PrepareBindings(bindings []any) []any

	// Transaction runs callback inside a transaction, retrying up to
	// attempts times on a deadlock. A Go method cannot be generic, so the
	// callback carries its result out through the closure.
	Transaction(callback func() error, attempts int) error

	// BeginTransaction opens a new transaction, or a nested savepoint if
	// one is already open.
	BeginTransaction() error

	// Commit commits the current transaction, or releases a savepoint.
	Commit() error

	// RollBack rolls the transaction back to toLevel, or back one level
	// when toLevel is nil.
	RollBack(toLevel *int) error

	// TransactionLevel reports how many transactions are currently nested.
	TransactionLevel() int

	// Pretend runs callback without executing its statements, and returns
	// the log of what would have run.
	Pretend(ctx context.Context, callback func(*Connection) error) ([]QueryLogEntry, error)

	// GetDatabaseName returns the name of the database this connection is
	// open on.
	GetDatabaseName() string
}

ConnectionInterface is everything that runs statements on a connection.

Every method takes a context.Context first, because a statement that cannot be cancelled holds a server connection for as long as the server likes, and returns an error rather than failing silently.

A caller that holds one of these is below the authorization layer, not outside it -- see the Connection doc for where the Grant lives and why it is not on these methods.

type ConnectionResolver

type ConnectionResolver struct {
	// contains filtered or unexported fields
}

ConnectionResolver is a map of name to connection, and the name of the default one.

It is the resolver for something that already has its connections -- a test, a worker built by hand, the capsule. DatabaseManager is the resolver that makes them on demand from configuration.

func NewConnectionResolver

func NewConnectionResolver(connections map[string]ConnectionInterface) *ConnectionResolver

NewConnectionResolver creates a ConnectionResolver over the given connections.

func (*ConnectionResolver) AddConnection

func (r *ConnectionResolver) AddConnection(name string, connection ConnectionInterface)

AddConnection registers connection under name.

func (*ConnectionResolver) Connection

func (r *ConnectionResolver) Connection(name string) (ConnectionInterface, error)

Connection returns the named connection, or the default when name is empty.

func (*ConnectionResolver) GetDefaultConnection

func (r *ConnectionResolver) GetDefaultConnection() string

GetDefaultConnection returns the default connection name.

func (*ConnectionResolver) HasConnection

func (r *ConnectionResolver) HasConnection(name string) bool

HasConnection reports whether a connection is registered under name.

func (*ConnectionResolver) SetDefaultConnection

func (r *ConnectionResolver) SetDefaultConnection(name string)

SetDefaultConnection replaces the default connection name.

type ConnectionResolverInterface

type ConnectionResolverInterface interface {
	// Connection returns the named connection. An empty name means the
	// default connection.
	Connection(name string) (ConnectionInterface, error)

	// GetDefaultConnection returns the default connection name.
	GetDefaultConnection() string

	// SetDefaultConnection replaces the default connection name.
	SetDefaultConnection(name string)
}

ConnectionResolverInterface answers a connection by name.

Connection returns an error for a name nobody registered, rather than a nil connection that fails four frames away.

type Connector

type Connector interface {
	// Dialect reports the connection dialect this connector supports.
	Dialect() Dialect

	// DriverName is the name the driver registered with database/sql. It is
	// read back from here rather than guessed, so a connector that switches
	// implementations does not need Open to be edited with it.
	DriverName() string
}

Connector links one engine's database/sql driver into the binary.

It is declared here rather than in the connectors package one level down: it names a Dialect, and a connectors package holding the interface would have to import this one while this one imports it back. Go refuses that cycle, and the interface is what moves.

A connector is a package with an init() and nothing to call. The project blank-imports the engines it speaks:

import (
    "github.com/arandu-io/hesape/database"
    _ "github.com/arandu-io/hesape/database/connectors/pgx"
    _ "github.com/arandu-io/hesape/database/connectors/sqlite"
)

and Open resolves the rest from DATABASE_URL. There is no second way to open a connection: a connector says which driver it linked, and never opens one itself.

type DB

type DB struct {
	// contains filtered or unexported fields
}

DB wraps *sql.DB to instrument the Collector and to rebind placeholders for the connection's dialect. Repositories use this type rather than *sql.DB, which is what makes every query show up on the debug page with the file and line that issued it.

It holds no driver import: the driver is chosen by the application, so the core keeps its two dependencies.

func Open

func Open(cfg Config) (*DB, func(), error)

Open connects, tunes the pool, and returns the instrumented handle plus the function that closes it.

The pool policy lives here rather than in each project's main, because it is not a preference: the defaults of database/sql are an unbounded pool, which turns one traffic spike into "too many connections" on the server instead of a queue in the process. What a project may choose is the size, through the three pool fields of Config; what it may not choose is having no size at all.

func Wrap

func Wrap(db *sql.DB, dialect Dialect) *DB

Wrap returns an instrumented handle over an open *sql.DB.

The dialect is what queries written with "?" are rebound to. An empty dialect means SQLite, which is the development default.

func (*DB) BeginTx

func (d *DB) BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error)

BeginTx starts a raw transaction on the underlying handle.

Prefer database.Transaction: statements run through this one are invisible to the Collector, and the outbox refuses to store events on it, because nothing connects it to the context that repositories read.

func (*DB) Delete added in v0.17.0

func (d *DB) Delete(ctx context.Context, statement string, bindings []any) (int64, error)

Delete runs a delete and returns the number of rows it removed.

func (*DB) Dialect

func (d *DB) Dialect() Dialect

Dialect reports the flavour this handle speaks. Repositories use it only when a statement genuinely cannot be written portably -- which should be rare, and is a smell worth explaining in a comment when it happens.

func (*DB) ExecContext

func (d *DB) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)

ExecContext runs a statement and records it, with the affected row count.

Inside database.Transaction it runs on the open transaction.

func (*DB) GetPostProcessor added in v0.17.0

func (d *DB) GetPostProcessor() query.Processor

GetPostProcessor returns the processor this handle's results are read back through: the one registered for its dialect.

func (*DB) GetQueryGrammar added in v0.17.0

func (d *DB) GetQueryGrammar() query.Grammar

GetQueryGrammar returns the grammar this handle's statements compile through: the one registered for its dialect.

func (*DB) Insert added in v0.17.0

func (d *DB) Insert(ctx context.Context, statement string, bindings []any) (bool, error)

Insert runs an insert, reporting whether it succeeded.

It does not report the identifier the engine assigned

There is no GetLastInsertID here, and its absence is the decision rather than an omission. A DB is the pool, shared by every request the process is serving, so "the identifier of the last insert" on it is whoever inserted most recently -- which is the classic way one request is handed another request's row. Storing it here would make that a field.

Nothing is lost by refusing. Identifiers in this framework are generated by the application, by NewID, into a text key -- an auto-incrementing column is not the shape a generated module has. Postgres, where one is used anyway, never asks: its processor compiles the insert with a returning clause and reads the identifier out of the result set, through Select. And a caller on an engine that does report one out of band gets a sentence naming exactly what is missing, from query/processors.ProcessInsertGetID, rather than a zero that reads like an identifier.

func (*DB) PingContext

func (d *DB) PingContext(ctx context.Context) error

PingContext verifies the connection. It feeds module health checks.

func (*DB) QueryContext

func (d *DB) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)

QueryContext runs a query and records it.

Inside database.Transaction it runs on the open transaction. That is what lets a repository written once work in both places, and what puts the outbox write in the same transaction as the row it describes.

func (*DB) QueryRowContext

func (d *DB) QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row

QueryRowContext runs a single-row query and records it.

The duration measured here covers issuing the query only: database/sql defers the actual work to Row.Scan, so a slow row shows up on the timeline as scan time rather than query time.

func (*DB) Select added in v0.17.0

func (d *DB) Select(ctx context.Context, statement string, bindings []any, _ bool) ([]query.Record, error)

Select runs a select and returns its rows.

The read-replica flag is accepted and ignored. A DB is one pool: there is no second one to send a select to instead, and a flag that silently selects the same pool is better than a signature that cannot be satisfied.

func (*DB) Statement added in v0.17.0

func (d *DB) Statement(ctx context.Context, statement string, bindings []any) (bool, error)

Statement runs a statement that returns neither rows nor a count, reporting whether it succeeded.

func (*DB) Unwrap

func (d *DB) Unwrap() *sql.DB

Unwrap returns the underlying handle, for the rare case that needs a driver specific feature. Prefer the wrapper: what goes through Unwrap does not show up on the debug page.

func (*DB) Update added in v0.17.0

func (d *DB) Update(ctx context.Context, statement string, bindings []any) (int64, error)

Update runs an update and returns the number of rows it changed.

type DatabaseManager

type DatabaseManager struct {
	// contains filtered or unexported fields
}

DatabaseManager is the resolver that makes connections on demand from configuration, keeps them, and hands the same one back next time.

Nothing is forwarded to the default connection: Connection(name) answers the connection, and the call goes on that. It is one more word and one fewer indirection, and it is the only shape a compiler can check.

func NewDatabaseManager

func NewDatabaseManager(config Configuration, factory *ConnectionFactory) *DatabaseManager

NewDatabaseManager creates a DatabaseManager over config, building connections with factory, or with a default ConnectionFactory when factory is nil.

func (*DatabaseManager) AvailableDrivers

func (m *DatabaseManager) AvailableDrivers() []string

AvailableDrivers returns the supported drivers that are also linked into the binary.

func (*DatabaseManager) Build

func (m *DatabaseManager) Build(config map[string]any) (*Connection, error)

Build opens a connection from a configuration nobody wrote in a file.

func (*DatabaseManager) ConnectUsing

func (m *DatabaseManager) ConnectUsing(name string, config map[string]any, force bool) (*Connection, error)

ConnectUsing opens a connection under name from config, failing if one already exists under that name unless force purges it first.

func (*DatabaseManager) Connection

func (m *DatabaseManager) Connection(name string) (ConnectionInterface, error)

Connection returns the named connection, creating it if this is the first request for it.

The name may carry a read/write suffix, `::read` or `::write`, which is what makes a replica addressable by name.

func (*DatabaseManager) ConnectionNames

func (m *DatabaseManager) ConnectionNames() []string

ConnectionNames returns the sorted names of the open connections.

Go's maps have no stable order, so a console table built from GetConnections would print its rows in a different order on every run without the sort.

func (*DatabaseManager) Disconnect

func (m *DatabaseManager) Disconnect(name string)

Disconnect closes the named connection's pools, if it is open.

func (*DatabaseManager) Extend

func (m *DatabaseManager) Extend(name string, resolver func(config map[string]any, name string) (*Connection, error))

Extend registers resolver as the connection constructor for name, either a connection name or a driver name.

func (*DatabaseManager) ForgetExtension

func (m *DatabaseManager) ForgetExtension(name string)

ForgetExtension removes the connection constructor registered for name.

func (*DatabaseManager) GetConnections

func (m *DatabaseManager) GetConnections() map[string]*Connection

GetConnections returns a copy of every connection the manager has made, keyed by name.

func (*DatabaseManager) GetDefaultConnection

func (m *DatabaseManager) GetDefaultConnection() string

GetDefaultConnection returns the configured default connection name.

func (*DatabaseManager) Purge

func (m *DatabaseManager) Purge(name string)

Purge disconnects and forgets the named connection.

func (*DatabaseManager) Reconnect

func (m *DatabaseManager) Reconnect(name string) (*Connection, error)

Reconnect closes and reopens the named connection, or opens it fresh if it was not already open.

func (*DatabaseManager) SetApplication

func (m *DatabaseManager) SetApplication(config Configuration) *DatabaseManager

SetApplication replaces the configuration the manager reads and writes the default connection name through.

func (*DatabaseManager) SetDefaultConnection

func (m *DatabaseManager) SetDefaultConnection(name string)

SetDefaultConnection replaces the configured default connection name.

func (*DatabaseManager) SetEventDispatcher

func (m *DatabaseManager) SetEventDispatcher(events Dispatcher) *DatabaseManager

SetEventDispatcher puts a dispatcher on every connection the manager makes.

It is set here once rather than looked up per connection.

func (*DatabaseManager) SetReconnector

func (m *DatabaseManager) SetReconnector(reconnector func(*Connection) error)

SetReconnector replaces the reconnector set on every connection the manager makes.

func (*DatabaseManager) SetTransactionManager

func (m *DatabaseManager) SetTransactionManager(manager *DatabaseTransactionsManager) *DatabaseManager

SetTransactionManager puts a transactions manager on every connection the manager makes.

func (*DatabaseManager) SupportedDrivers

func (m *DatabaseManager) SupportedDrivers() []string

SupportedDrivers returns the dialects this package can open.

func (*DatabaseManager) UsingConnection

func (m *DatabaseManager) UsingConnection(name string, callback func() error) error

UsingConnection runs callback with a different default connection, and puts the old one back afterward.

type DatabaseTransactionRecord

type DatabaseTransactionRecord struct {
	// Connection is the connection's name.
	Connection string

	// Level is the transaction's nesting level.
	Level int

	// Parent is the transaction this one was opened inside.
	Parent *DatabaseTransactionRecord
	// contains filtered or unexported fields
}

DatabaseTransactionRecord is one open transaction, and the callbacks waiting on how it ends.

func NewDatabaseTransactionRecord

func NewDatabaseTransactionRecord(connection string, level int, parent *DatabaseTransactionRecord) *DatabaseTransactionRecord

NewDatabaseTransactionRecord creates a DatabaseTransactionRecord.

func (*DatabaseTransactionRecord) AddCallback

func (r *DatabaseTransactionRecord) AddCallback(callback func())

AddCallback registers callback to run after this transaction commits.

func (*DatabaseTransactionRecord) AddCallbackForRollback

func (r *DatabaseTransactionRecord) AddCallbackForRollback(callback func())

AddCallbackForRollback registers callback to run after this transaction rolls back.

func (*DatabaseTransactionRecord) ExecuteCallbacks

func (r *DatabaseTransactionRecord) ExecuteCallbacks()

ExecuteCallbacks runs every callback registered with AddCallback.

func (*DatabaseTransactionRecord) ExecuteCallbacksForRollback

func (r *DatabaseTransactionRecord) ExecuteCallbacksForRollback()

ExecuteCallbacksForRollback runs every callback registered with AddCallbackForRollback.

func (*DatabaseTransactionRecord) GetCallbacks

func (r *DatabaseTransactionRecord) GetCallbacks() []func()

GetCallbacks returns the callbacks registered with AddCallback.

func (*DatabaseTransactionRecord) GetCallbacksForRollback

func (r *DatabaseTransactionRecord) GetCallbacksForRollback() []func()

GetCallbacksForRollback returns the callbacks registered with AddCallbackForRollback.

type DatabaseTransactionsManager

type DatabaseTransactionsManager struct {
	// contains filtered or unexported fields
}

DatabaseTransactionsManager remembers which transactions are open on which connection, and runs the callbacks that were waiting for the outermost one to commit.

It is what makes AfterCommit mean what it says. A queue job dispatched inside a transaction that then rolls back is a job about a row that does not exist, and it is the classic way a background worker starts failing on data nobody can find.

func NewDatabaseTransactionsManager

func NewDatabaseTransactionsManager() *DatabaseTransactionsManager

NewDatabaseTransactionsManager creates a DatabaseTransactionsManager.

func (*DatabaseTransactionsManager) AddCallback

func (m *DatabaseTransactionsManager) AddCallback(callback func())

AddCallback runs callback after the outermost transaction commits, or now when there is none.

func (*DatabaseTransactionsManager) AddCallbackForRollback

func (m *DatabaseTransactionsManager) AddCallbackForRollback(callback func())

AddCallbackForRollback runs callback if the current transaction rolls back.

Outside a transaction it does nothing, and that is right: there is no rollback coming for a statement that already committed.

func (*DatabaseTransactionsManager) AfterCommitCallbacksShouldBeExecuted

func (m *DatabaseTransactionsManager) AfterCommitCallbacksShouldBeExecuted(level int) bool

AfterCommitCallbacksShouldBeExecuted reports whether level is the outermost transaction level, the only one whose commit runs the staged callbacks.

func (*DatabaseTransactionsManager) Begin

func (m *DatabaseTransactionsManager) Begin(connection string, level int)

Begin records that a new transaction level opened on connection.

func (*DatabaseTransactionsManager) CallbackApplicableTransactions

func (m *DatabaseTransactionsManager) CallbackApplicableTransactions() []*DatabaseTransactionRecord

CallbackApplicableTransactions returns a copy of the pending transaction records a callback could be added to.

func (*DatabaseTransactionsManager) Commit

func (m *DatabaseTransactionsManager) Commit(connection string, levelBeingCommitted, newTransactionLevel int)

Commit records that a transaction level committed on connection, and runs the callbacks staged for it once the outermost transaction is the one committing.

The callbacks run only then, which is the whole guarantee: a savepoint that commits inside a transaction that later rolls back has not committed anything.

func (*DatabaseTransactionsManager) GetCommittedTransactions

func (m *DatabaseTransactionsManager) GetCommittedTransactions() []*DatabaseTransactionRecord

GetCommittedTransactions returns a copy of the records staged to run their callbacks.

func (*DatabaseTransactionsManager) GetPendingTransactions

func (m *DatabaseTransactionsManager) GetPendingTransactions() []*DatabaseTransactionRecord

GetPendingTransactions returns a copy of every open transaction record.

func (*DatabaseTransactionsManager) Rollback

func (m *DatabaseTransactionsManager) Rollback(connection string, newTransactionLevel int)

Rollback records that connection rolled back to newTransactionLevel, and runs the rollback callbacks for every level undone.

func (*DatabaseTransactionsManager) StageTransactions

func (m *DatabaseTransactionsManager) StageTransactions(connection string, levelBeingCommitted int)

StageTransactions moves the pending records of a connection at or above a level into the committed list.

type DeadlockException

type DeadlockException = concerns.DeadlockError

DeadlockException reports that the engine chose this transaction as the deadlock victim.

It is an alias rather than a declaration because ManagesTransactions is what raises it and lives in the concerns package, which this one imports -- so declaring the type here would close the cycle. An alias is one type under two names, so errors.As works whichever name a caller reaches for.

func NewDeadlockException

func NewDeadlockException(previous error) *DeadlockException

NewDeadlockException answers `new DeadlockException(...)`.

type Dialect

type Dialect string

Dialect is the SQL flavour of a connection.

The names are the conventional DB_CONNECTION values, so an .env reads the way somebody expects it to.

const (
	// DialectSQLite is the default for local development: a file, no server,
	// nothing to install.
	DialectSQLite Dialect = "sqlite"
	// DialectPostgres is the production target.
	DialectPostgres Dialect = "pgsql"
	// DialectMySQL is supported and is not the recommendation. Every query here
	// is written with "?", which MySQL takes directly, so nothing about the SQL
	// changes; what changes is that Postgres is where the migration story, the
	// transactional DDL and the outbox relay are least surprising.
	//
	// The conformance suite runs the generated queries against a real server on
	// all three, MySQL included.
	DialectMySQL Dialect = "mysql"
)

Supported dialects.

func ParseDialect

func ParseDialect(v string) (Dialect, error)

ParseDialect validates a DB_CONNECTION value.

func Registered

func Registered() []Dialect

Registered reports the dialects this binary can speak, sorted.

`aru doctor` reads it, and so does the error below.

func (Dialect) Driver

func (d Dialect) Driver() string

Driver is the database/sql driver name a dialect expects to be registered under. The application imports the driver; the framework only names it, which is what keeps the core free of database dependencies.

func (Dialect) Rebind

func (d Dialect) Rebind(query string) string

Rebind translates the portable "?" placeholder into what the dialect expects.

Every query in this framework is written with "?", the form SQLite and MySQL use, and Postgres gets "$1, $2, ..." here. That is the entire portability layer: there is no query builder, and the SQL you read in a repository is the SQL that runs. Anything beyond placeholders -- a type, a function -- is the repository's job to keep portable.

Placeholders inside string literals are left alone, because '?' is an ordinary character in a LIKE pattern or in seeded data.

type Dispatcher

type Dispatcher interface {
	// Dispatch fires an event.
	Dispatch(event any)
}

Dispatcher is what a connection needs of an event dispatcher, narrowed to the one method it calls.

It is declared here rather than imported from hesape/events so that the SQL package keeps its own dependency list, which is the same reason every other interface in this component is declared where it is consumed.

type LostConnectionDetector

type LostConnectionDetector struct{}

LostConnectionDetector reads a driver error and says whether the connection is gone.

It is a list of substrings and nothing cleverer, because every driver reports this differently and several report it more than one way. The list is deliberately wide: an entry that never matches costs one string comparison, and a missed one costs a request.

func NewLostConnectionDetector

func NewLostConnectionDetector() *LostConnectionDetector

NewLostConnectionDetector creates a LostConnectionDetector.

func (*LostConnectionDetector) CausedByLostConnection

func (d *LostConnectionDetector) CausedByLostConnection(err error) bool

CausedByLostConnection reports whether err indicates that the connection is gone, checking both this detector's message list and the messages the Go drivers use.

type LostConnectionException

type LostConnectionException struct {
	// Message is the sentence NewLostConnectionException was given.
	Message string
}

LostConnectionException reports that the connection went away and there was no reconnector to get it back.

func NewLostConnectionException

func NewLostConnectionException(message string) *LostConnectionException

NewLostConnectionException answers `new LostConnectionException($message)`.

func (*LostConnectionException) Error

func (e *LostConnectionException) Error() string

Error is the message.

type MapConfiguration

type MapConfiguration map[string]any

MapConfiguration is a Configuration backed by a map, for a test or a worker wired by hand.

func (MapConfiguration) Get

func (m MapConfiguration) Get(key string) any

Get answers Configuration.Get.

func (MapConfiguration) Set

func (m MapConfiguration) Set(key string, value any)

Set answers Configuration.Set.

type MigrationResolver

type MigrationResolver struct {
	// Resolver is the resolver being adapted: a DatabaseManager, or a
	// ConnectionResolver built by hand.
	Resolver ConnectionResolverInterface
}

MigrationResolver adapts a ConnectionResolverInterface to migrations.Resolver, which is the same adaptation one level up.

func (MigrationResolver) Connection

func (r MigrationResolver) Connection(name string) (migrations.Connection, error)

Connection satisfies migrations.Resolver.Connection: it resolves the named connection and adapts it with ForMigrations, or fails if it is not a *Connection.

func (MigrationResolver) GetDefaultConnection

func (r MigrationResolver) GetDefaultConnection() string

GetDefaultConnection satisfies migrations.Resolver.GetDefaultConnection, forwarding to the wrapped resolver.

func (MigrationResolver) SetDefaultConnection

func (r MigrationResolver) SetDefaultConnection(name string)

SetDefaultConnection satisfies migrations.Resolver.SetDefaultConnection, forwarding to the wrapped resolver.

type MultipleRecordsFoundException

type MultipleRecordsFoundException = concerns.MultipleRecordsFoundError

MultipleRecordsFoundException is what Sole returns when the query matched more than one row, and it carries how many it saw.

It is an alias of concerns.MultipleRecordsFoundError, not a second type: a value built by either package satisfies errors.As under both names. See that type for what the count means.

func NewMultipleRecordsFoundException

func NewMultipleRecordsFoundException(count int) *MultipleRecordsFoundException

NewMultipleRecordsFoundException answers its constructor.

type Page

type Page[T any] struct {
	Items []T
	Next  string
}

Page is what List returns: the rows, and the cursor that reaches the next page.

It replaced a bare []T, which could not say whether there was more. The caller had to infer it from len(items) == q.Limit, which is wrong exactly once per result set -- on the page whose last row is the last row.

Next is opaque and goes straight back into Query.Cursor. It is the empty string on the last page, which is the whole of the "is there more" question:

for q := database.Query{Limit: 100}; ; {
    page, err := repo.List(ctx, g, q)
    ...
    if page.Next == "" {
        break
    }
    q.Cursor = page.Next
}

It is not a paginator and does not become one. The links, the page numbers and the per-page window a template draws live in hesape/pagination, which builds them from rows somebody already fetched. This type is the repository's half: what came back, and where to resume.

type Query

type Query struct {
	Limit  int
	Cursor string
	Sort   string
}

Query is pagination and ordering with an allowlist. The sort field is NEVER interpolated directly: the repository validates it against a permitted set, or ordering becomes injection through another door.

There is no Filter here, and that is a decision rather than an omission.

The field existed, exported, with no producer and no consumer in any of the ten repositories: List(ctx, g, Query{Filter: ...}) returned the whole list, with no error and no warning. A field that silently does nothing is worse than one that does not exist -- the reader assumes it filtered, and in an application where rows belong to tenants that assumption is how a leak starts.

Filling it in would mean a generic predicate language over columns, which is a query builder, which is a second way to reach data. A module that needs to read by something other than the id declares the method it needs on its own repository, with the SQL written out and the values in placeholders.

type QueryException

type QueryException struct {
	// ConnectionName is the connection's name.
	ConnectionName string

	// SQL is the query as it was issued.
	SQL string

	// Bindings are the values that went with it, already through
	// PrepareBindings.
	Bindings []any

	// ConnectionDetails is driver, name, host, port, database, unix_socket.
	ConnectionDetails map[string]any

	// ReadWriteType is "read", "write", or empty.
	ReadWriteType string

	// Previous is the driver error this exception wraps.
	Previous error
}

QueryException reports that a statement failed, and says which one, on which connection, with which values.

The message is assembled in three parts: the driver's own sentence, then the connection with its host, port and database, then the SQL with the bindings written into the placeholders. That last part is why this type exists at all -- "syntax error at or near $3" names nothing a person can act on, and the same error with the values in it usually names the mistake.

func NewQueryException

func NewQueryException(connectionName, sql string, bindings []any, previous error, connectionDetails map[string]any, readWriteType string) *QueryException

NewQueryException creates a QueryException.

func (*QueryException) Error

func (e *QueryException) Error() string

Error formats the driver's message, the connection and the query with its bindings written in, in that order.

func (*QueryException) GetBindings

func (e *QueryException) GetBindings() []any

GetBindings returns the values that went with the query.

func (*QueryException) GetConnectionDetails

func (e *QueryException) GetConnectionDetails() map[string]any

GetConnectionDetails returns the driver, name, host, port, database and socket the connection reported.

func (*QueryException) GetConnectionName

func (e *QueryException) GetConnectionName() string

GetConnectionName returns the connection's name.

func (*QueryException) GetRawSQL

func (e *QueryException) GetRawSQL() string

GetRawSQL answers the statement with its bindings written in.

Nothing has to be looked up to build it: the bindings are already on the exception.

func (*QueryException) GetSQL

func (e *QueryException) GetSQL() string

GetSQL returns the query as it was issued.

func (*QueryException) Unwrap

func (e *QueryException) Unwrap() error

Unwrap makes errors.Is and errors.As reach the driver error.

type QueryLogEntry

type QueryLogEntry struct {
	// Query is the SQL as it was issued.
	Query string

	// Bindings are the values that went with it.
	Bindings []any

	// Time is how long it took, in milliseconds, the unit every listener
	// compares against.
	Time float64

	// ReadWriteType is "read", "write", or empty.
	ReadWriteType string
}

QueryLogEntry is one row of the query log a Connection keeps.

type Repository

type Repository[T any, ID comparable] interface {
	Find(ctx context.Context, g auth.Grant, id ID) (T, error)
	List(ctx context.Context, g auth.Grant, q Query) (Page[T], error)
	Create(ctx context.Context, g auth.Grant, entity T) (T, error)
	Update(ctx context.Context, g auth.Grant, entity T) (T, error)
	Delete(ctx context.Context, g auth.Grant, id ID) error
}

Repository is the contract every module repository implements.

Look at the signature: auth.Grant is mandatory and comes before the id, on a read exactly as on a write.

Three things hold this up, and they are not the same thing

The compiler guarantees you hold a Grant. auth.Grant has only unexported fields, so it cannot be written as a struct literal: a call to any method here that omits it does not compile, and neither does one that tries to build a valid one by hand.

`aru doctor` guarantees it is the right Grant. auth.Authorize is the path where a Policy answered, and it is not the only exported way to obtain one -- auth.SystemGrant issues a Grant for work that has no subject, and the queue's GrantFor reissues one for a job. So a handler can hold a Grant no Policy was ever asked about. What reports that is a lint, not the type system.

Convention guarantees you came through here at all. DB.QueryContext, DB.ExecContext and DB.QueryRowContext are exported, take no Grant, and reach the same rows. They take none because they could not use one: they are handed a finished string, and a parameter that looks like enforcement while filtering nothing is worse than no parameter. So a module reads rows through a Repository, and a module that reaches them through the connection is a module that gets sent back in review.

This comment used to say that a Grant cannot be constructed outside the auth package, and therefore that no path from a handler to the database skips a Policy. Neither half was true, and stating it wrong here costs more than elsewhere: this is the interface every module implements, so it is the doc a reader checks the claim against.

type Seeder

type Seeder[D any] interface {
	// Name is how the seeder is addressed on the command line.
	Name() string
	// Run performs the seeding. It must be safe to run twice: a seeder that
	// fails on the second run cannot be part of a deploy.
	Run(ctx context.Context, d D) error
}

Seeder is one unit of seeding.

The shape is a DatabaseSeeder that calls the others, and `aru db:seed <Name>` to run one. A seeder is a type satisfying an interface rather than something discovered, so one that does not compile is caught at build time and not when somebody runs it against production.

D is whatever the project decided a seeder is allowed to touch. It stays in the project (arandu/database/seeders) rather than being declared here, because "allowed to touch" is the application's answer and not the framework's: a seeder that can reach anything is a seeder nobody can review. What lives here is only the contract and the naming, which is what every project repeated.

type Tx

type Tx struct {
	// contains filtered or unexported fields
}

Tx is an instrumented transaction.

Statements run through it are recorded on the Collector exactly like the ones outside, which matters more than it sounds: a query that only misbehaves inside a transaction is the one nobody can see on the debug page.

type UniqueConstraintViolationException

type UniqueConstraintViolationException struct{ QueryException }

UniqueConstraintViolationException is a QueryException, which it embeds, narrowed to a violated unique constraint.

It exists so firstOrCreate and its neighbours can catch the one failure they expect -- two requests inserting the same row at the same time -- without catching every other statement error with it.

func NewUniqueConstraintViolationException

func NewUniqueConstraintViolationException(connectionName, sql string, bindings []any, previous error, connectionDetails map[string]any, readWriteType string) *UniqueConstraintViolationException

NewUniqueConstraintViolationException answers its constructor, which is QueryException's.

Directories

Path Synopsis
Package capsule is the database usable from a script with no application around it: three lines of configuration, and a connection.
Package capsule is the database usable from a script with no application around it: three lines of configuration, and a connection.
Package concerns holds the shared halves of a connection and a query builder: ManagesTransactions, BuildsQueries, BuildsWhereDateClauses, CompilesJsonPaths, ExplainsQueries and ParsesSearchPath.
Package concerns holds the shared halves of a connection and a query builder: ManagesTransactions, BuildsQueries, BuildsWhereDateClauses, CompilesJsonPaths, ExplainsQueries and ParsesSearchPath.
Package conformance is the suite every connector has to pass against a real server.
Package conformance is the suite every connector has to pass against a real server.
Package connectors is the parent of the three connector modules.
Package connectors is the parent of the three connector modules.
mysql module
pgx module
sqlite module
Package console holds the database commands: db, db:monitor, db:show, db:table, db:wipe and model:prune.
Package console holds the database commands: db, db:monitor, db:show, db:table, db:wipe and model:prune.
factories
Package factories will hold make:factory, and holds nothing yet.
Package factories will hold make:factory, and holds nothing yet.
migrations
Package migrations holds the eight migration commands as console.Command values, with the flags --database, --path, --pretend, --step, --batch, --seed and --force.
Package migrations holds the eight migration commands as console.Command values, with the flags --database, --path, --pretend, --step, --batch, --seed and --force.
seeds
Package seeds holds db:seed and make:seeder.
Package seeds holds db:seed and make:seeder.
Package criteria translates a query string into clauses a declared query accepts, and refuses everything else.
Package criteria translates a query string into clauses a declared query accepts, and refuses everything else.
Package events holds the values the database dispatches: connection established, query executed, statement prepared, the four transaction events, the migration events, the schema dump and load events, and model pruning.
Package events holds the values the database dispatches: connection established, query executed, statement prepared, the four transaction events, the migration events, the schema dump and load events, and model pruning.
Package migrations runs schema changes: the Migration contract, the registry they declare themselves in, the migrator that applies them and the repository table that records what ran.
Package migrations runs schema changes: the Migration contract, the registry they declare themselves in, the migrator that applies them and the repository table that records what ran.
Package model holds the Model, its query Builder, its Collection and soft deletes.
Package model holds the Model, its query Builder, its Collection and soft deletes.
attributes
Package attributes declares nothing, and will not.
Package attributes declares nothing, and will not.
factories
Package factories builds rows of an entity for tests and for seeding.
Package factories builds rows of an entity for tests and for seeding.
relations
Package relations holds the sixteen relation types, and the eager loading that keeps them from being N+1 queries.
Package relations holds the sixteen relation types, and the eager loading that keeps them from being N+1 queries.
relations/concerns
Package concerns holds the shared halves the sixteen relation types are assembled from: AsPivot, CanBeOneOfMany, ComparesRelatedModels, InteractsWithDictionary, InteractsWithPivotTable, SupportsDefaultModels and SupportsInverseRelations.
Package concerns holds the shared halves the sixteen relation types are assembled from: AsPivot, CanBeOneOfMany, ComparesRelatedModels, InteractsWithDictionary, InteractsWithPivotTable, SupportsDefaultModels and SupportsInverseRelations.
Package query builds SQL: the Builder, the raw Expression, the index hint and the join clause.
Package query builds SQL: the Builder, the raw Expression, the index hint and the join clause.
grammars
Package grammars holds one grammar per engine, each one compiling a *query.Builder into the SQL that engine speaks: MySQLGrammar, MariaDBGrammar, PostgresGrammar and SQLiteGrammar, over the shared Grammar in grammar.go.
Package grammars holds one grammar per engine, each one compiling a *query.Builder into the SQL that engine speaks: MySQLGrammar, MariaDBGrammar, PostgresGrammar and SQLiteGrammar, over the shared Grammar in grammar.go.
processors
Package processors holds the hook a driver takes to adjust results on the way out of the connection: MySQLProcessor, MariaDBProcessor, PostgresProcessor and SQLiteProcessor over the shared Processor.
Package processors holds the hook a driver takes to adjust results on the way out of the connection: MySQLProcessor, MariaDBProcessor, PostgresProcessor and SQLiteProcessor over the shared Processor.
Package schema declares tables and indexes: the Blueprint a migration writes against, the Builder that executes it, and the dump-and-load schema state.
Package schema declares tables and indexes: the Blueprint a migration writes against, the Builder that executes it, and the dump-and-load schema state.
grammars
Package grammars turns a schema.Blueprint into statements, one grammar per engine: MySQLGrammar, PostgresGrammar and SQLiteGrammar over BaseGrammar.
Package grammars turns a schema.Blueprint into statements, one grammar per engine: MySQLGrammar, PostgresGrammar and SQLiteGrammar over BaseGrammar.

Jump to

Keyboard shortcuts

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