Databases

[ AxiomCore · Documentation ]

Database contracts

Reference for Acore database contract files, versions, PostgreSQL and SQLite schemas, connections, the SQL console, commands and diagnostics.

A database contract is an Acore source file that declares a database's desired schema and an immutable version. This page lists the syntax, version rules, connection options, commands, generated files and diagnostics for PostgreSQL and SQLite contracts in Axiom CLI 0.149.0 and later (current release 0.149.1). For the step-by-step workflow, see Migrations and persistence.

Files and identity

FilePurpose
AxiomDeps.tomltype = "database" and targets = ["server"] select the database compiler. No profile header is needed in source.
Entry .acore fileproject(id: ..., version: ...), a module line and one database block.
migration.acore (optional)Typed data changes, referenced from the entry file with migration(source: "migration.acore"). See Typed data changes.
database/AxiomDeps.toml
type = "database"
targets = ["server"]

Identity rules:

  • project(version: ...) is the contract and migration version. It has three numeric parts, such as 1.0.0. Leading zeros and pre-release suffixes are rejected with ADB1100.
  • Object identities stay stable across version-only changes.
  • A legacy manifest [application] table can still supply identity for offline reads. If source and manifest both supply identity, they must agree. Publishing a new migration requires project(...) in source.

Declare a schema

database/schema.acore
project(id: "example.tasks.storage", version: "1.0.0")
module example.tasks.storage

database tasks(engine: DatabaseEngine.postgres) {
  manage(schemas: [Schemas.app], mode: ManagementMode.ownedObjects)
  schema app {
    table tasks {
      id: Pg.Text()
      title: Pg.VarChar(length: 120)
      primaryKey(columns: [.id])
    }
  }
}
database/schema.acore
project(id: "example.tasks.sqlite", version: "1.0.0")
module example.tasks.sqlite

database tasks(engine: DatabaseEngine.sqlite) {
  manage(schemas: [Schemas.main], mode: ManagementMode.ownedObjects)
  schema main {
    table tasks(strict: true) {
      id: Sqlite.Text()
      title: Sqlite.Text()
      primaryKey(columns: [.id])
    }
  }
}

manage(...) names the schemas Axiom owns. Add nullable: true to a column type, for example note: Sqlite.Text(nullable: true), to allow nulls. primaryKey(..., sqlName: "tasks_pkey") sets the physical constraint name that storage declarations can refer to.

SQLite storage types

TypeUse
Sqlite.Text, Sqlite.Integer, Sqlite.Int, Sqlite.Real, Sqlite.Blob, Sqlite.AnySQLite storage class and affinity
Sqlite.Native(declared: ...)An explicit declared type
  • table name(strict: true) creates a STRICT table, which accepts only strict-compatible declared types.
  • Keys, foreign keys, checks, defaults, generated columns, indexes and supported views and triggers use the dedicated SQLite catalog.
  • The managed scope is the entire main schema. Attached databases are not supported.
  • SQLite INTEGER values reach the backend through lossless string codecs. TEXT is the simplest fit for identifiers such as the task example's.
  • Table rebuilds keep retained values and validate a consistent populated backup before the target changes. Foreign-key and native integrity checks run before acceptance.
  • Unsupported type, rowid or sequence conversions fail explicitly. Axiom never silently coerces an existing identity.
  • The internal ledger and the schema commit atomically under a writer lock. Native locking, hot-journal and WAL recovery, and the original qualified SQLite lifecycle are qualified separately from compiler-only evidence.

PostgreSQL types and objects

Columns use Pg types such as Pg.Text() and Pg.VarChar(length: 120). A schema can also declare grants, row security and a managed application role:

Acore excerpt: inside database tasks
schema app {
  table tasks {
    tenant: Pg.Text()
    id: Pg.Text()
    title: Pg.VarChar(length: 120)
    primaryKey(columns: [.tenant, .id], sqlName: "tasks_pkey")
  }
  grant taskAccess(objectId: Tables.app.tasks, roleIds: [Roles.tasks_app], public: false,
    privileges: ["select", "insert", "update", "delete"], columns: [], grantOption: false)
  rowSecurity taskIsolation(tableId: Tables.app.tasks, enabled: true, force: true)
}
role tasks_app(managed: true, login: true, inherit: true, superuser: false,
  createDb: false, createRole: false, replication: false, bypassRls: false,
  connectionLimit: 20, credentialRef: "tasks_app")

credentialRef is an opaque slot. Axiom generates the role's credential and stores it privately, apart from the operator connection. Advanced PostgreSQL declarations (SQL and PL/pgSQL objects, grants and row-level security, partitions and typed package imports) keep their qualified language profiles. Engine capability diagnostics apply before runtime execution. See Capability boundaries.

Version rules

  • Every published pair of contract ID and version binds to immutable content.
  • Rerunning unchanged content is a no-op: no migration, no prompt and no new version. Comment-only edits are not schema changes.
  • Changed content under a version that is already used, or under a version that is not newer than a published one, fails with ADB1103 and a suggested version. The suggestion bumps the patch number, or the major number (for example 2.0.0) when the change is destructive.
  • In an interactive terminal, press Enter to accept the suggestion. The CLI changes only the version literal in source and keeps your formatting. If AxiomDeps.toml still has a legacy [application] table, the same edit removes it. The edit is saved only after validation passes.
  • The prompt appears only when stdin and stdout are terminals, without --non-interactive or --json, and when CI is unset, empty, 0 or false. Otherwise pass --auto-version.
  • A release that changes only the version is shown as metadata-only.
  • Accepting a version is separate from approving risky changes (see Risk approvals).
  • The developer history holds up to 4,096 revisions. Beyond that, use the qualified checkpoint profile.

Connect to a database

OptionEngineMeaning
--database-url URLBothpostgres:// or postgresql://; or sqlite:./tasks.sqlite, sqlite:///absolute/tasks.sqlite
DATABASE_URLBothUsed when you pass no connection flag
--host, --port, --database, --usernamePostgreSQLEndpoint fields. --port defaults to 5432. PGDATABASE and PGUSER (then USER) fill omitted fields.
--passwordPostgreSQLPrefer PGPASSWORD or the masked prompt
--ssl-modePostgreSQLdisable, prefer, require or verify-full
--tls-ca FILEPostgreSQLTrust a private certificate authority
--database-file PATHSQLiteA file path, relative to the current directory
Terminal: connect with fields instead of a URL
axiom run database/schema.acore --host localhost --port 5432 \
  --database tasks_demo --username tasks_admin

PostgreSQL rules:

  • The password prompt is masked. A URL and endpoint fields cannot be combined. PGHOST, PGPORT and PGSSLMODE are not read.
  • A noninteractive run needs PGPASSWORD, --password or a secret-injected URL (ADB1110 otherwise).
  • Remote hosts use verified TLS unless you pass --ssl-mode disable. prefer never falls back to plaintext. Loopback hosts (localhost, 127.0.0.1, ::1) connect without TLS unless you pass require or verify-full.
  • Credentials are redacted from connection diagnostics.
  • migrate and run need a writable primary. status and history can read a read-only target and open read-only sessions.
  • The target must already be reachable. Before applying a change, Axiom replays it on a disposable PostgreSQL of the same major version. It uses the managed validator shipped in the Linux CLI archives (PostgreSQL 17), otherwise a matching installation found through AXIOM_DATABASE_POSTGRES_BIN, PG_BIN, PATH, pg_config or standard Homebrew and Linux locations. With none, the command fails with ADB1106 and nothing is applied. You do not run pg_ctl, psql or OpenSSL yourself, and you do not write binding JSON or development-profile files.

SQLite rules:

  • With no file, no URL and no DATABASE_URL, the CLI uses .axiom/database/data/database.sqlite under the project root (the nearest directory with AxiomDeps.toml). Unset a PostgreSQL DATABASE_URL first, and use the same file for every command and for backend startup.
  • migrate and run create a missing file with owner-only permissions. status stays read-only and fails if the file is missing.
  • :memory: and symbolic links are rejected. A PostgreSQL URL or PostgreSQL fields with a SQLite contract fail with ADB1107 (engine mismatch).
  • The CLI finds the native SQLite library itself; set AXIOM_SQLITE_LIBRARY to override. No sqlite3 command, library flag or binding JSON is needed.
  • SQLite acceptance is recorded on macOS arm64 and Linux arm64, separately from production certification. On non-Unix platforms the CLI reports that the native SQLite lifecycle is not qualified.

SQL console

axiom run CONTRACT applies pending versions, verifies the target and opens a native SQL console:

Terminal
axiom run database/schema.acore --database-url "$DATABASE_URL"
axiom run database/schema.acore --database-file ./tasks.sqlite
InputEffect
SQLMultiline statements, quoted semicolons and comments
\dtList tables
\d app.tasks (SQLite: \d tasks)Describe a table
\conninfoShow the connection. For SQLite this includes the file path for DBeaver or another viewer.
\q or Ctrl+D at an empty promptExit
Up and DownRecall input from this session (last 100 entries)
Ctrl+CClear pending input, or cancel the running query

For scripts, apply and query without the console:

Terminal
axiom run database/schema.acore --database-url "$DATABASE_URL" --no-shell \
  --execute 'SELECT * FROM app.tasks;'

If the console opens while stdin is not a terminal, it runs all of stdin as a SQL script instead of prompting. --execute runs after synchronization. The console still opens unless you add --no-shell (or --once, --json or --dry-run). The script is split at unquoted semicolons and each statement runs on its own, not in one transaction. The first error stops the script with a non-zero exit.

LimitPostgreSQLSQLite
Timeouts15 s per statement, 15 s to connect5 s busy timeout
Result sizeNo row cap20,000 rows, 256 columns, 4 MiB per BLOB and 64 MiB per statement; BLOBs print as base64

Commands

CommandConnectsPurpose
axiom database check SOURCENoCheck the entry file and its imports. Prints JSON evidence.
axiom database fmt SOURCENoFormat source to stdout, or in place with --write
axiom database build SOURCENoWrite a canonical server-only schema package (--out FILE to choose the path)
axiom database inspect SOURCENoSummarize a source or schema package
axiom database diff A [B]NoCompare two sources or packages, or catalog versions with --from and --to
axiom database migrate CONTRACTYesApply pending versions
axiom database status CONTRACTYes, read-onlyApplied and working versions, pending changes and drift
axiom database history [SOURCE]Yes, read-onlyVersion history, including rollbacks and failed attempts
axiom database rollback CONTRACTYesRestore a recorded schema through a new version
axiom run CONTRACTYesMigrate, then open the SQL console

axiom check and axiom build also accept a database source. history keeps advanced subcommands for exported histories: export, checkpoint, verify (offline; grants no apply authority), import and import-atlas (adopt a verified Atlas revision snapshot as private provenance). axiom database lists every flag.

Offline output

Terminal: task-app
axiom check database/sqlite/schema.acore
axiom database build database/sqlite/schema.acore
Output
Valid database contract: database/sqlite/schema.acore
{"artifact":".../database/sqlite/.axiom/contracts/database.axiom","effects":"offline-only","format":"axiom-database-build/v1","migrationAuthority":false,"profile":"acore-database/v1","sha256":"..."}

axiom database check prints JSON evidence (objects, columns and source positions); inspect reports "liveState": "not-observed". The two-file diff prints one entry per change, with a path such as Columns.main.tasks.note, an operation and a compatibility class. diff --from VERSION reads the local migration catalog (ADB1102 if the version is missing); --to defaults to working.

Risk approvals

Before applying, the preview classifies destructive, data-dependent and security changes. Interactive approval is separate from version acceptance. A noninteractive run must authorize each category that applies:

FlagAuthorizes
--allow-destructiveChanges that may delete data or remove invariants, such as dropping a column
--allow-data-dependentChanges that depend on populated data or rebuild existing invariants
--allow-security-changesChanges to database permissions or security policy

There is no blanket approval flag. Approvals authorize a change. They do not invent missing values and they do not restore deleted data. A required new column on populated rows needs a default, a nullable expansion or a declared backfill; Axiom rejects missing values before running target DDL. DDL applied outside Axiom is reported as drift and is never silently absorbed or removed.

Generated files and state

LocationContentsKeep in version control
.axiom/contracts/database.axiom and the database.sources.axiom source capsule from buildsNo; rebuilt on demand
.axiom/database/migrations/VERSION/release.axiomImmutable, non-secret migration definitionsYes, or keep exact CI artifacts
.axiom/database/ (other files)Private plans, bindings, locks, reports and source-edit recovery journals, owner-onlyNo
.axiom/database/data/database.sqliteThe default persistent SQLite databaseNo, but keep it: it is data, not cache
Private secret store outside the projectSigning keys and role credentialsNever

Legacy root locks and identity sidecars stay readable during an upgrade.

Existing schemas

A managed scope that already contains objects fails with ADB1105. To take ownership, pass --adopt with a reviewed baseline: Axiom enrolls the scope only when it exactly matches the validated source. Existing certified ledgers and history stay intact and keep their recovery procedures. See the existing database guide.

Diagnostics

CodeMeaningWhat to do
ADB1000Invalid application schema or storage mapping, such as an unknown table or columnFix the backend storage declaration and rebuild
ADB1100Version is not MAJOR.MINOR.PATCHUse a version such as 1.0.1
ADB1102Version not in the catalog, nothing applied, or rollback steps out of rangeCheck history; supply the catalog or bundle
ADB1103Version already used, or frozen mode without a prepared releaseAccept the suggested version or pass --auto-version
ADB1104Another command is publishing, or the migration parent changedRetry after it finishes; check status and prepare again
ADB1105Drift, target not current (status --check), unenrolled objects or an adoption mismatchInspect drift; use a reviewed --adopt baseline
ADB1106Validator unavailable or validation mismatch; transition needs the certified nontransactional profileInstall a matching PostgreSQL; nothing was applied
ADB1107Invalid URL, engine mismatch, unusable SQLite file or read-only target for a writeFix the connection options
ADB1109Signing or credential problem, including a signer that differs from the enrolled targetRestore the original signing provider
ADB1110PostgreSQL password requiredUse the prompt, PGPASSWORD or --password
ADB1113Rollback crosses a data overwrite whose previous values were not archivedWrite a compensating migration or restore a backup
ADB1121Typed data change rejectedUse a supported backfill or a certified adapter