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:
- an auth.Grant required by every operation (the mandatory path);
- tenant scoping taken from the Grant, never from a parameter;
- automatic instrumentation into the Collector;
- 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
- Variables
- func AfterCommit(ctx context.Context, db *DB, fn func(context.Context)) error
- func AvailableDrivers() []string
- func CalculateDynamicConnectionName(config map[string]any) string
- func CausedByConcurrencyError(err error) bool
- func CausedByLostConnection(err error) bool
- func Day(t time.Time) time.Time
- func DriverName(d Dialect) (string, error)
- func DriverTitle(driver string) string
- func Flag(args []string, name string) (string, bool)
- func ForMigrations(connection *Connection) migrations.Connection
- func ForSchema(connection *Connection) schema.Connection
- func GetResolver(driver string) func(pdo *sql.DB, database, prefix string, config map[string]any) *Connection
- func InTransaction(ctx context.Context, db *DB) bool
- func NewID() (string, error)
- func NewSQLiteDatabaseDoesNotExistException(path string) error
- func Register(c Connector)
- func ResolverFor(driver string, ...)
- func Seed[D any](ctx context.Context, registry []Seeder[D], fallback string, args []string, ...) (string, error)
- func SupportedDrivers() []string
- func Switch(args []string, name string) bool
- func Transaction(ctx context.Context, db *DB, fn func(context.Context) error) error
- func TransactionAt(ctx context.Context, db *DB, level sql.IsolationLevel, ...) error
- type ConcurrencyErrorDetector
- type Config
- type Configuration
- type ConfigurationUrlParser
- type Connection
- func (c *Connection) AffectingStatement(ctx context.Context, q string, bindings []any) (int64, error)
- func (c *Connection) AllowQueryDurationHandlersToRunAgain()
- func (c *Connection) BeforeExecuting(callback func(query string, bindings []any, connection *Connection)) *Connection
- func (c *Connection) BindValues(bindings []any) []any
- func (c *Connection) CausedByConcurrencyError(err error) bool
- func (c *Connection) CausedByLostConnection(err error) bool
- func (c *Connection) CommitTransactionStatement() error
- func (c *Connection) CompileSavepoint(name string) string
- func (c *Connection) CompileSavepointRollBack(name string) string
- func (c *Connection) Cursor(ctx context.Context, q string, bindings []any, useReadPDO bool) func(yield func(query.Record, error) bool)
- func (c *Connection) Delete(ctx context.Context, q string, bindings []any) (int64, error)
- func (c *Connection) DisableQueryLog()
- func (c *Connection) Disconnect()
- func (c *Connection) EnableQueryLog()
- func (c *Connection) Escape(value any, binary bool) (string, error)
- func (c *Connection) ExecuteBeginTransactionStatement() error
- func (c *Connection) ExecuteSavepointStatement(statement string) error
- func (c *Connection) FireConnectionEvent(event string)
- func (c *Connection) FlushQueryLog()
- func (c *Connection) ForgetRecordModificationState()
- func (c *Connection) GetConfig(option string) any
- func (c *Connection) GetConnectionDetails() map[string]any
- func (c *Connection) GetDatabaseName() string
- func (c *Connection) GetDriverName() string
- func (c *Connection) GetDriverTitle() string
- func (c *Connection) GetEventDispatcher() Dispatcher
- func (c *Connection) GetName() string
- func (c *Connection) GetNameWithReadWriteType() string
- func (c *Connection) GetPDO() (*sql.DB, error)
- func (c *Connection) GetPostProcessor() query.Processor
- func (c *Connection) GetQueryGrammar() query.Grammar
- func (c *Connection) GetQueryLog() []QueryLogEntry
- func (c *Connection) GetRawPDO() *sql.DB
- func (c *Connection) GetRawQueryLog() []QueryLogEntry
- func (c *Connection) GetRawReadPDO() *sql.DB
- func (c *Connection) GetReadPDO() (*sql.DB, error)
- func (c *Connection) GetSchemaBuilder() any
- func (c *Connection) GetSchemaGrammar() any
- func (c *Connection) GetServerVersion(ctx context.Context) (string, error)
- func (c *Connection) GetTablePrefix() string
- func (c *Connection) HasModifiedRecords() bool
- func (c *Connection) Insert(ctx context.Context, q string, bindings []any) (bool, error)
- func (c *Connection) InsertReturningID(ctx context.Context, q string, bindings []any) (int64, error)
- func (c *Connection) IsUniqueConstraintError(err error) bool
- func (c *Connection) Listen(callback func(*dbevents.QueryExecuted))
- func (c *Connection) LogQuery(q string, bindings []any, timeMS float64)
- func (c *Connection) Logging() bool
- func (c *Connection) PrepareBindings(bindings []any) []any
- func (c *Connection) Pretend(ctx context.Context, callback func(*Connection) error) ([]QueryLogEntry, error)
- func (c *Connection) Pretending() bool
- func (c *Connection) Query() *query.Builder
- func (c *Connection) Raw(value any) query.Expression
- func (c *Connection) Reconnect() error
- func (c *Connection) ReconnectIfMissingConnection() error
- func (c *Connection) RecordsHaveBeenModified(value bool)
- func (c *Connection) ResetTotalQueryDuration()
- func (c *Connection) RollBackTransactionStatement() (bool, error)
- func (c *Connection) Scalar(ctx context.Context, q string, bindings []any, useReadPDO bool) (any, error)
- func (c *Connection) Select(ctx context.Context, q string, bindings []any, useReadPDO bool) ([]query.Record, error)
- func (c *Connection) SelectFromWriteConnection(ctx context.Context, q string, bindings []any) ([]query.Record, error)
- func (c *Connection) SelectOne(ctx context.Context, q string, bindings []any, useReadPDO bool) (query.Record, bool, error)
- func (c *Connection) SelectResultSets(ctx context.Context, q string, bindings []any, useReadPDO bool) ([][]query.Record, error)
- func (c *Connection) SetDatabaseName(database string) *Connection
- func (c *Connection) SetEventDispatcher(dispatcher Dispatcher) *Connection
- func (c *Connection) SetPDO(pdo *sql.DB) *Connection
- func (c *Connection) SetPostProcessor(processor query.Processor) *Connection
- func (c *Connection) SetQueryGrammar(grammar query.Grammar) *Connection
- func (c *Connection) SetReadPDO(pdo *sql.DB) *Connection
- func (c *Connection) SetReadPDOConfig(config map[string]any) *Connection
- func (c *Connection) SetReadWriteType(readWriteType string) *Connection
- func (c *Connection) SetReconnector(reconnector func(*Connection) error) *Connection
- func (c *Connection) SetRecordModificationState(value bool) *Connection
- func (c *Connection) SetSchemaGrammar(grammar any) *Connection
- func (c *Connection) SetTablePrefix(prefix string) *Connection
- func (c *Connection) Statement(ctx context.Context, q string, bindings []any) (bool, error)
- func (c *Connection) SubstituteBindingsIntoRawSQL(sql string, bindings []any) string
- func (c *Connection) SupportsSavepoints() bool
- func (c *Connection) Table(table any, as ...string) *query.Builder
- func (c *Connection) ThreadCount(ctx context.Context) (int64, error)
- func (c *Connection) TotalQueryDuration() float64
- func (c *Connection) Unprepared(ctx context.Context, q string) (bool, error)
- func (c *Connection) UnsetEventDispatcher()
- func (c *Connection) Update(ctx context.Context, q string, bindings []any) (int64, error)
- func (c *Connection) UseDefaultPostProcessor()
- func (c *Connection) UseDefaultQueryGrammar()
- func (c *Connection) UseDefaultSchemaGrammar()
- func (c *Connection) UseWriteConnectionWhenReading(value bool) *Connection
- func (c *Connection) WhenQueryingForLongerThan(threshold time.Duration, handler func(*Connection, *dbevents.QueryExecuted))
- func (c *Connection) WithoutPretending(callback func() error) error
- func (c *Connection) WithoutTablePrefix(callback func(*Connection) error) error
- type ConnectionFactory
- func (f *ConnectionFactory) CreateConnection(driver string, pool *sql.DB, database, prefix string, config map[string]any) (*Connection, error)
- func (f *ConnectionFactory) CreateConnector(config map[string]any) (Connector, error)
- func (f *ConnectionFactory) Make(config map[string]any, name string) (*Connection, error)
- type ConnectionInterface
- type ConnectionResolver
- func (r *ConnectionResolver) AddConnection(name string, connection ConnectionInterface)
- func (r *ConnectionResolver) Connection(name string) (ConnectionInterface, error)
- func (r *ConnectionResolver) GetDefaultConnection() string
- func (r *ConnectionResolver) HasConnection(name string) bool
- func (r *ConnectionResolver) SetDefaultConnection(name string)
- type ConnectionResolverInterface
- type Connector
- type DB
- func (d *DB) BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error)
- func (d *DB) Delete(ctx context.Context, statement string, bindings []any) (int64, error)
- func (d *DB) Dialect() Dialect
- func (d *DB) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
- func (d *DB) GetPostProcessor() query.Processor
- func (d *DB) GetQueryGrammar() query.Grammar
- func (d *DB) Insert(ctx context.Context, statement string, bindings []any) (bool, error)
- func (d *DB) PingContext(ctx context.Context) error
- func (d *DB) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
- func (d *DB) QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
- func (d *DB) Select(ctx context.Context, statement string, bindings []any, _ bool) ([]query.Record, error)
- func (d *DB) Statement(ctx context.Context, statement string, bindings []any) (bool, error)
- func (d *DB) Unwrap() *sql.DB
- func (d *DB) Update(ctx context.Context, statement string, bindings []any) (int64, error)
- type DatabaseManager
- func (m *DatabaseManager) AvailableDrivers() []string
- func (m *DatabaseManager) Build(config map[string]any) (*Connection, error)
- func (m *DatabaseManager) ConnectUsing(name string, config map[string]any, force bool) (*Connection, error)
- func (m *DatabaseManager) Connection(name string) (ConnectionInterface, error)
- func (m *DatabaseManager) ConnectionNames() []string
- func (m *DatabaseManager) Disconnect(name string)
- func (m *DatabaseManager) Extend(name string, ...)
- func (m *DatabaseManager) ForgetExtension(name string)
- func (m *DatabaseManager) GetConnections() map[string]*Connection
- func (m *DatabaseManager) GetDefaultConnection() string
- func (m *DatabaseManager) Purge(name string)
- func (m *DatabaseManager) Reconnect(name string) (*Connection, error)
- func (m *DatabaseManager) SetApplication(config Configuration) *DatabaseManager
- func (m *DatabaseManager) SetDefaultConnection(name string)
- func (m *DatabaseManager) SetEventDispatcher(events Dispatcher) *DatabaseManager
- func (m *DatabaseManager) SetReconnector(reconnector func(*Connection) error)
- func (m *DatabaseManager) SetTransactionManager(manager *DatabaseTransactionsManager) *DatabaseManager
- func (m *DatabaseManager) SupportedDrivers() []string
- func (m *DatabaseManager) UsingConnection(name string, callback func() error) error
- type DatabaseTransactionRecord
- func (r *DatabaseTransactionRecord) AddCallback(callback func())
- func (r *DatabaseTransactionRecord) AddCallbackForRollback(callback func())
- func (r *DatabaseTransactionRecord) ExecuteCallbacks()
- func (r *DatabaseTransactionRecord) ExecuteCallbacksForRollback()
- func (r *DatabaseTransactionRecord) GetCallbacks() []func()
- func (r *DatabaseTransactionRecord) GetCallbacksForRollback() []func()
- type DatabaseTransactionsManager
- func (m *DatabaseTransactionsManager) AddCallback(callback func())
- func (m *DatabaseTransactionsManager) AddCallbackForRollback(callback func())
- func (m *DatabaseTransactionsManager) AfterCommitCallbacksShouldBeExecuted(level int) bool
- func (m *DatabaseTransactionsManager) Begin(connection string, level int)
- func (m *DatabaseTransactionsManager) CallbackApplicableTransactions() []*DatabaseTransactionRecord
- func (m *DatabaseTransactionsManager) Commit(connection string, levelBeingCommitted, newTransactionLevel int)
- func (m *DatabaseTransactionsManager) GetCommittedTransactions() []*DatabaseTransactionRecord
- func (m *DatabaseTransactionsManager) GetPendingTransactions() []*DatabaseTransactionRecord
- func (m *DatabaseTransactionsManager) Rollback(connection string, newTransactionLevel int)
- func (m *DatabaseTransactionsManager) StageTransactions(connection string, levelBeingCommitted int)
- type DeadlockException
- type Dialect
- type Dispatcher
- type LostConnectionDetector
- type LostConnectionException
- type MapConfiguration
- type MigrationResolver
- type MultipleRecordsFoundException
- type Page
- type Query
- type QueryException
- func (e *QueryException) Error() string
- func (e *QueryException) GetBindings() []any
- func (e *QueryException) GetConnectionDetails() map[string]any
- func (e *QueryException) GetConnectionName() string
- func (e *QueryException) GetRawSQL() string
- func (e *QueryException) GetSQL() string
- func (e *QueryException) Unwrap() error
- type QueryLogEntry
- type Repository
- type Seeder
- type Tx
- type UniqueConstraintViolationException
Constants ¶
const DefaultSQLitePath = "database/database.sqlite"
DefaultSQLitePath is where a fresh project keeps its database file.
const DefaultURL = "sqlite://" + DefaultSQLitePath
DefaultURL is what a project with no DATABASE_URL runs on: a file, no server, nothing installed.
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 ¶
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.
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.
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.
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.
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.
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.
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.
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.
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
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
DriverTitle answers the human name of a driver, and the driver itself for one nobody named.
func Flag ¶
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 ¶
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 ¶
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 ¶
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 Transaction ¶
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 ¶
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 ¶
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) Redacted ¶
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 ¶
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.
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 ¶
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 ¶
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) 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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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
Delete runs a delete and returns the number of rows it removed.
func (*DB) 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 ¶
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
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
GetQueryGrammar returns the grammar this handle's statements compile through: the one registered for its dialect.
func (*DB) Insert ¶ added in v0.17.0
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 ¶
PingContext verifies the connection. It feeds module health checks.
func (*DB) QueryContext ¶
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 ¶
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
Statement runs a statement that returns neither rows nor a count, reporting whether it succeeded.
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.
Source Files
¶
- config.go
- configurationurlparser.go
- connection.go
- connectioninterface.go
- connector.go
- dbmodel.go
- defaultgrammars.go
- detectors.go
- dialect.go
- doc.go
- errors.go
- factory.go
- keytext.go
- manager.go
- migrationadapter.go
- open.go
- repository.go
- resolver.go
- schemaadapter.go
- schemarecords.go
- schemarecordvalues.go
- seeder.go
- transactionsmanager.go
- tx.go
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. |