Documentation
¶
Overview ¶
Package data defines the data access contract.
There is no ORM. Queries are plain parameterized SQL -- generated by sqlc from .sql files once phase 2 lands -- which makes SQL injection impossible and the query plan predictable. What this package adds on top is:
- a security.Grant required by every operation (the mandatory path);
- tenant scoping taken from the Grant, never from a parameter;
- automatic instrumentation into the Collector.
Index ¶
- Constants
- func Migrate(ctx context.Context, db *DB, migrations []Migration) ([]string, error)
- func Rollback(ctx context.Context, db *DB, migrations []Migration) ([]string, error)
- func Tenant(g security.Grant) string
- type AppliedMigration
- type DB
- func (d *DB) BeginTx(ctx context.Context, opts *sql.TxOptions) (*sql.Tx, error)
- func (d *DB) Dialect() Dialect
- func (d *DB) ExecContext(ctx context.Context, query string, args ...any) (sql.Result, 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) Unwrap() *sql.DB
- type Dialect
- type Migration
- type Query
- type Repository
Constants ¶
const MigrationsTable = "arandu_migrations"
MigrationsTable is where applied migration ids are recorded.
Variables ¶
This section is empty.
Functions ¶
func Migrate ¶
Migrate applies the pending migrations, in the given order, and returns the ids it applied.
Everything applied by one call shares a batch number, which is what makes Rollback undo a deploy rather than a single migration -- the same model Laravel uses, and the reason its rollback is useful in practice.
Each migration runs inside its own transaction together with the insert into the tracking table, so a failure halfway cannot leave the schema ahead of the record. It stops at the first failure: applying later migrations over a broken schema turns one clear error into an unrecoverable database.
Types ¶
type AppliedMigration ¶ added in v0.2.0
AppliedMigration is one row of the tracking table.
func AppliedMigrations ¶
func AppliedMigrations(ctx context.Context, db *DB) ([]AppliedMigration, error)
AppliedMigrations returns the tracking table, ordered by batch and id.
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 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 transaction on the underlying handle.
Statements inside the transaction are not recorded by the Collector yet: that needs a wrapper over *sql.Tx, which arrives with the generated repositories in phase 2.
func (*DB) Dialect ¶ added in v0.2.0
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.
func (*DB) PingContext ¶
PingContext verifies the connection. It feeds module health checks.
func (*DB) QueryContext ¶
QueryContext runs a query and records it.
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.
type Dialect ¶ added in v0.2.0
type Dialect string
Dialect is the SQL flavour of a connection.
The names match Laravel's DB_CONNECTION values, because the .env of an Arandu project is meant to be readable by someone arriving from there.
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 accepted by the connection layer. Repositories are not // portable to it yet: MySQL has no RETURNING, so every insert needs a second // statement. See docs/adr/0009. DialectMySQL Dialect = "mysql" )
Supported dialects.
func ParseDialect ¶ added in v0.2.0
ParseDialect validates a DB_CONNECTION value.
func (Dialect) Driver ¶ added in v0.2.0
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 ¶ added in v0.2.0
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 Migration ¶
Migration is a versioned, immutable-once-published schema change.
The id carries its own order -- "2026_07_29_000001_create_users_table" -- for the same reason Laravel names its files that way: a migration that sorts differently on two machines applies in a different order on two machines.
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.
type Repository ¶
type Repository[T any, ID comparable] interface { Find(ctx context.Context, g security.Grant, id ID) (T, error) List(ctx context.Context, g security.Grant, q Query) ([]T, error) Create(ctx context.Context, g security.Grant, entity T) (T, error) Update(ctx context.Context, g security.Grant, entity T) (T, error) Delete(ctx context.Context, g security.Grant, id ID) error }
Repository is the contract every module repository implements.
Look at the signature: security.Grant is mandatory and comes before the id. Because a Grant cannot be constructed outside the security package, there is no path from a handler to the database that skips a Policy.