data

package
v0.27.0 Latest Latest
Warning

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

Go to latest
Published: Aug 17, 2026 License: MIT Imports: 5 Imported by: 0

Documentation

Overview

Package data defines the data access contract.

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

  1. a security.Grant required by every operation (the mandatory path);
  2. tenant scoping taken from the Grant, never from a parameter;
  3. automatic instrumentation into the Collector.

This package is a bridge. It is removed in v1.0.0; import github.com/arandu-io/hesape/database directly.

The components moved to github.com/arandu-io/hesape, where the package is called database, and this package is now the old name pointing at them. It answers to two hesape packages:

hesape/database  Repository, Query, DB, Dialect, Transaction, Migration
hesape/auth      Tenant

The death date above is what keeps this from being a second way to import one type. Nothing here holds an implementation: where the name and the signature survived the move it is a Go alias, and where the design diverged it is an envelope that translates and nothing more.

Tenant is the one symbol that answers to another package

Tenant is one line -- g.Subject().Tenant -- and living in the package that owns the SQL forced the cache and the filesystem to import the database in order to read a tenant. It is auth.Tenant instead. The guarantee does not move with it: the tenant still comes from the Grant, never from a path, a body, a query or a header.

The one envelope, and what diverged

Repository is declared here rather than aliased, because hesape/database changed the return of List from ([]T, error) to (Page[T], error). See the type for the whole argument.

Index

Constants

View Source
const (
	// DialectSQLite is the default for local development: a file, no server,
	// nothing to install.
	DialectSQLite = database.DialectSQLite
	// DialectPostgres is the production target.
	DialectPostgres = database.DialectPostgres
	// 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.
	DialectMySQL = database.DialectMySQL
)

Supported dialects.

View Source
const KeyText = database.KeyText

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. VARCHAR(255) is accepted by all three.

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

View Source
const MigrationsTable = database.MigrationsTable

MigrationsTable is where applied migration ids are recorded.

Variables

This section is empty.

Functions

func Day added in v0.10.0

func Day(t time.Time) time.Time

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

It exists because DATE is the one type in the portable subset that the three engines do not agree about: PostgreSQL drops the time part on write and SQLite stores whatever the driver sent, so the same code, on the same day, returns different values depending on the engine. `aru make:module` emits it for every field declared as a date.

func InTransaction added in v0.3.0

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

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

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

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

func Migrate

func Migrate(ctx context.Context, db *DB, migrations []Migration) ([]string, error)

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. 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 is a pipeline step and never a call on the way up: with N replicas, N migrations race.

func NewID added in v0.3.0

func NewID() (string, error)

NewID returns a version 4 UUID as text.

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

The bytes and the format are hesape's. The only difference a caller can observe is the prefix on the error the reader returns when the system is out of entropy, which reads "database:" rather than "data:".

func Rollback added in v0.2.0

func Rollback(ctx context.Context, db *DB, migrations []Migration) ([]string, error)

Rollback undoes the last batch and returns the ids it reverted, most recent first.

A migration with an empty Down is refused rather than skipped: silently leaving part of a batch in place is how a rollback produces a schema that matches neither version.

func Tenant

func Tenant(g auth.Grant) string

Tenant returns the tenant from the Grant. Every multi-tenant statement must take this value, never a tenant that came in with the request.

Renamed on the way to hesape only in address: it is auth.Tenant there, with the same signature and the same one line of body.

func Transaction added in v0.3.0

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

Transaction runs fn inside a database transaction.

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

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

Types

type AppliedMigration added in v0.2.0

type AppliedMigration = database.AppliedMigration

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.

func Status added in v0.2.0

func Status(ctx context.Context, db *DB, migrations []Migration) ([]AppliedMigration, error)

Status returns every declared migration with the batch it was applied in, or zero when it is still pending. It is what `aru migrate:status` prints.

type DB

type DB = database.DB

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.

An alias, so a handle opened by hesape/database.Open is the handle a repository written against this package takes.

func Wrap

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

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

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

type Dialect added in v0.2.0

type Dialect = database.Dialect

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. Driver and Rebind are methods on the hesape type and come across with the alias.

func ParseDialect added in v0.2.0

func ParseDialect(v string) (Dialect, error)

ParseDialect validates a DB_CONNECTION value.

type Migration

type Migration = database.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 reason that convention exists: a migration that sorts differently on two machines applies in a different order on two machines.

func Pending

func Pending(ctx context.Context, db *DB, migrations []Migration) ([]Migration, error)

Pending returns the migrations that have not been applied yet, in order.

type Query

type Query = database.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 argument is on hesape/database.Query, where the field was deleted.

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) ([]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: the Grant is mandatory and comes before the id. Because a Grant cannot be constructed outside the authorization package, there is no path from a handler to the database that skips a Policy -- on the way out as much as on the way in.

It stays declared here rather than aliasing hesape/database.Repository, which changed the return of List from ([]T, error) to (Page[T], error) so that a caller can tell "this is the last page" from "this page happens to be full". The better shape is hesape's; the alias is still refused. Every module repository the generator emits carries a `var _ data.Repository[T, string] = (*R)(nil)` line -- framework/modules/auth, aru's templates and the three repositories in examples -- and an alias would break all of them at once, in a package none of them can see, with an error about a return type they never wrote. A bridge that changes a signature is not a bridge.

The migration is the caller's, one repository at a time: change List to answer database.Page[T] and drop this contract for hesape's. Nothing in hesape consumes either shape, so the two can coexist until v1.0.0 removes this one.

The Grant here is auth.Grant, which is what security.Grant already is -- the security bridge aliases it -- so a repository written against either name satisfies this.

type Tx added in v0.3.0

type Tx = database.Tx

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.

Jump to

Keyboard shortcuts

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