Add a hectic.immutable parent table. Tables inheriting it get auto-attached BEFORE INSERT/UPDATE/DELETE/TRUNCATE row+statement triggers that block DML unless the session sets hectic.migration_mode='on' (intended use: SET LOCAL inside a migration transaction). Same exemptions as the rest of the bundle apply (hectic schema, partitions, temp tables, GUC-excluded schemas). database diff now appends an --- IMMUTABLE TABLE DATA --- section to its output, with per-table unified row diffs of every table inheriting hectic.immutable, surfacing drift in 'frozen' reference data alongside schema drift. Subcommand exits non-zero when either schema or data differs. Test postgres-init-hectic-inheritance extended to 10 cases covering immutable triggers, DML blocked outside migration_mode, SET LOCAL allowing DML inside a transaction, GUC not leaking past COMMIT, and TRUNCATE under migration_mode.
db-tool
PostgreSQL development database management tool. Drop-in replacement for per-project database.sh / postgres-init.sh / postgres-cleanup.sh scripts. Provides database, postgres-init, and postgres-cleanup binaries.
Provided Binaries
| Binary | Description |
|---|---|
database |
Main script for managing migrations, deployments, and logs. |
postgres-init |
Ephemeral PostgreSQL cluster initialization and startup. |
postgres-cleanup |
Graceful shutdown and cleanup of the PostgreSQL cluster. |
Required Environment Variables
These variables must be set for db-tool to function.
| Variable | Description |
|---|---|
LOCAL_DIR |
Absolute path to the project root directory. |
DB_URL |
Full PostgreSQL connection string (e.g., postgresql://user@localhost/dbname?host=$PG_WORKING_DIR). |
PG_WORKING_DIR |
Directory where the PostgreSQL cluster data and sockets are stored. |
Optional Environment Variables
| Variable | Default Value | Description |
|---|---|---|
DATABASE_DIR |
${LOCAL_DIR}/db |
Root directory for database-related files. |
MIGRATION_DIR |
${DATABASE_DIR}/migration |
Directory containing SQL migration files. |
DATABASE_SOURCE |
${DATABASE_DIR}/src |
Directory containing source SQL files for hydration. |
PG_URL_VAR |
PGURL |
The name of the environment variable where the computed PG URL will be exported. |
PG_LOG_PATH |
(unset) | Path to redirect PostgreSQL server logs. |
PG_CONF_FILE |
(unset) | Path to a postgresql.conf file. When set, replaces the script-generated config entirely on fresh init. port and unix_socket_directories are still appended at runtime (always overridden). When set, PG_DISABLE_LOGGING and PG_SHARED_PRELOAD_LIBRARIES are ignored. |
PG_SHARED_PRELOAD_LIBRARIES |
pg_cron |
Comma-separated shared_preload_libraries value. Set to empty string to disable. Ignored when PG_CONF_FILE is set. |
PG_DISABLE_LOGGING |
0 |
Set to 1 to disable PostgreSQL logging collector. Ignored when PG_CONF_FILE is set. |
PG_HECTIC_INHERITANCE |
1 |
Apply the hectic inheritance bundle to the target database after init. Set to 0 to disable. |
HECTIC_INHERITANCE_SQL |
(auto) | Override path to the SQL file applied by PG_HECTIC_INHERITANCE=1. Defaults to the SQL shipped with postgres-init. |
PATCH_LOG |
(stdout) | Path to log the output of database patches. |
HYDRATE_LOG |
(stdout) | Path to log the output of database hydration. |
Postgres Package Override
By default, db-tool/postgres-init/postgres-cleanup use plain postgresql_17 from nixpkgs. If you need extensions (e.g. pg_cron), override the postgres package per-output:
let
myPg = pkgs.postgresql_17.withJIT.withPackages (_: [
pkgs.postgresql_17.pkgs.pg_cron
]);
in {
packages = [
(pkgs.hectic."db-tool".override { postgresql = myPg; })
(pkgs.hectic."postgres-init".override { postgresql = myPg; })
(pkgs.hectic."postgres-cleanup".override { postgresql = myPg; })
];
}
pull_staging Contract
The pull_staging subcommand allows importing data from a remote staging environment into the local test-data.sql file. This functionality requires four specific environment variables to be defined:
STAGING_SSH_HOST: The SSH destination for the staging server.STAGING_DB_URL: The PostgreSQL connection string for the remote staging database.STAGING_DUMP_TABLES: A space-separated list of tables to include in the data dump.STAGING_DUMP_FLAGS: Additional flags to pass topg_dump(e.g.,--column-inserts).
If any of these variables are missing when pull_staging is invoked, the tool will exit with code 3 and print the name of the missing variable to stderr.
Subcommands
deploy: Execute the full deployment flow (hydrate + patch). Supports--cleanupto teardown after success.log: Inspect database logs. Supportslistand index-based selection.test: Execute database tests located in${DATABASE_DIR}/test/test.sql.check: Run a deployment validation in an isolated, temporary PostgreSQL cluster.cleanup: Stop the local database cluster and remove thePG_WORKING_DIR.pull_staging: Import data from the staging environment based on the env contract.init: Wrapper aroundpostgres-initto start the cluster.migrator: Directly invoke the migration tool with the correct environment context.
shellHook Example
To use db-tool in a Nix development shell, add the following to your flake.nix or shell.nix:
{
# ...
devShells.default = pkgs.mkShell {
packages = [
pkgs.hectic.db-tool
pkgs.hectic.postgres-init
pkgs.hectic.postgres-cleanup
];
shellHook = ''
export LOCAL_DIR="$PWD"
export DATABASE_DIR="$LOCAL_DIR/db"
export MIGRATION_DIR="$DATABASE_DIR/migration"
export DATABASE_SOURCE="$DATABASE_DIR/src"
export PG_WORKING_DIR="$LOCAL_DIR/focus/postgresql"
export DB_URL="postgresql://user@localhost/dbname?host=$PG_WORKING_DIR&port=5432"
# for other non-db scripts (deploy.sh, task.sh, etc.):
export HECTIC_LIB="${pkgs.hectic.helpers.posix-shell.log}"
# Initialize and start the ephemeral database cluster
. ${pkgs.hectic.postgres-init}/bin/postgres-init
'';
};
}
hectic Inheritance Bundle
pkgs.hectic.hectic-inheritance ships a SQL artifact that bootstraps a hectic
schema with three parent tables and DDL event triggers:
-
hectic.created_at(created_at TIMESTAMPTZ NOT NULL DEFAULT NOW())— every user table mustINHERITS (hectic.created_at). The event triggerhectic_enforce_created_at_inheritanceraises an exception onCREATE TABLEotherwise. -
hectic.updated_at(updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW())— optional. Any table that inherits from it automatically gets aBEFORE UPDATE FOR EACH ROWtrigger callinghectic.set_updated_at()attached byhectic_attach_updated_at_trigger. -
hectic.immutable()— pure marker. Tables inheriting it are blocked fromINSERT/UPDATE/DELETE/TRUNCATEoutside migration mode by triggers attached byhectic_attach_immutable_triggers. Useful for reference data that must only change via migrations. To allow DML inside a migration, wrap it in a transaction:BEGIN; SET LOCAL hectic.migration_mode = 'on'; INSERT INTO public.frozen (id, label) VALUES (1, 'x'); COMMIT;SET LOCALis required so the permission cannot leak pastCOMMIT.
Always-exempt schemas: hectic, information_schema, anything matching
pg_*. Declarative partitions (relispartition = true) and temporary tables
are also auto-exempt.
Per-database opt-out for additional schemas via the
hectic.inheritance_extra_excluded_schemas GUC (comma-separated):
ALTER DATABASE mydb SET hectic.inheritance_extra_excluded_schemas = 'legacy,etl';
db-tool diff and immutable tables
database diff already includes immutable tables in its schema-level
comparison (via pg_dump --schema-only). On top of that, when a hectic
schema is present in either side, it appends an
--- IMMUTABLE TABLE DATA --- section to the diff with a per-table unified
diff of the rows of every table inheriting hectic.immutable. Drift in
"frozen" reference data therefore surfaces in the same pager view as schema
drift, and the subcommand exits non-zero when either differs.
Apply via postgres-init
Applied automatically. Set PG_HECTIC_INHERITANCE=0 to opt out.
Apply via migrator or any psql pipeline
# in your devshell
shellHook = ''
export HECTIC_INHERITANCE_SQL=${pkgs.hectic.hectic-inheritance}/share/hectic/hectic-inheritance.sql
'';
psql "$DB_URL" -v ON_ERROR_STOP=1 -f "$HECTIC_INHERITANCE_SQL"
The SQL is also exposed via self.lib.hecticInheritance.sql (string) and
self.lib.hecticInheritance.path (Nix path) for inline pipelines.
Exit Codes
| Code | Meaning |
|---|---|
| 1 | Generic error. |
| 2 | Ambiguous arguments or state. |
| 3 | Missing required argument or environment variable. |
| 5 | Provided table does not exist. |
| 9 | Argument or command not found. |
| 13 | Program bug or unexpected system state. |
| 127 | Command not found (missing dependency). |