Status: proposed. This describes the next round of changes to sql-file. It's meant to be read before the work starts, and kept up to date as decisions change.
Out of scope: a real SQL parser (statements are split the way
sql-file.script/sql-statements splits them now), and other database
engines.
A schema id is a string such as ectcommon/0001-session-key or
todo/0003-item-tags. Its script is the resource <id>.sql, looked
up on the connection's :schema-path, the same search sql-file does
today. Ids may contain / to organize scripts into directories; they
may not contain whitespace. Ids are stored in a VARCHAR(255).
A "chain" is now just a naming convention: a series of scripts sharing a prefix, each requiring the one before.
Directives live in the script's leading comment block, before the first statement, one per line:
-- sql-file: requires ectcommon/0001-session-key
-- sql-file: requires todo/0003-item-tags
-- sql-file: replaces toto-16
requires <id>: the named schema must be installed before this one.
A script may have any number of these; their order is significant
(see Installation order).replaces <id>: as in 0.5.0, but naming a schema id. The 0.5.0
two-argument form, replaces <name> <n>, is read as
replaces <name>-<n>.Any other -- sql-file: line in the leading block is an error, as
now.
Existing scripts named schema-<name>-<n>.sql keep working unchanged:
<name>-<n> (for example toto-16).<name>-<n> whose <id>.sql isn't found is
looked up as schema-<name>-<n>.sql.n > 0 implicitly requires <name>-<n-1>, in
addition to anything it declares.A legacy request, the vector ["todo" 3] in :schemas, is read as
the id "todo-3". This is a transition aid for existing applications;
it can be removed in a later release once nothing uses it.
sql-file's own tables replace sql_file_schema.
CREATE CACHED TABLE sql_file_installed (
schema_id VARCHAR(255) NOT NULL PRIMARY KEY,
how VARCHAR(16) NOT NULL, -- 'run', 'recorded', or 'migrated'
requires VARCHAR(4096) NULL, -- effective requires, newline separated
replaces VARCHAR(4096) NULL, -- newline separated
digest CHAR(64) NULL, -- hex SHA-256; NULL until known
statements_tracked BOOLEAN DEFAULT FALSE NOT NULL,
started_on TIMESTAMP NULL,
completed_on TIMESTAMP NULL -- NULL while running, or if it failed
);
CREATE CACHED TABLE sql_file_statement ( -- phase 4
schema_id VARCHAR(255) NOT NULL,
ordinal INTEGER NOT NULL,
digest CHAR(64) NOT NULL,
completed_on TIMESTAMP NOT NULL,
PRIMARY KEY (schema_id, ordinal)
);
how is run for a script that was executed, recorded for one
recorded under replaces without running, and migrated for rows
carried over from sql_file_schema (below).requires and replaces makes the
installed graph self-describing. sql-file can check it against the
requested scripts even when old script files have since been deleted.recorded and migrated rows, started_on equals
completed_on (the time they were recorded), or both are NULL for
migrated rows.sql-file's own schema stays an internal, legacy-style chain, managed before anything the application asks for:
schema-sql-file-0.sql (existing): creates sql_file_schema.schema-sql-file-1.sql (new): creates sql_file_installed and
sql_file_statement.sql-file-1 is installed, code (not SQL; it needs to expand
ranges) copies sql_file_schema: a row (name, n) becomes rows
<name>-0 through <name>-<n> with how = 'migrated', no times
and no digest. sql-file-0 and sql-file-1 themselves are recorded
the same way.sql_file_schema is kept up to date for numbered schemas (sql-file's
own row stays at 0), so an application can be rolled back to an
older sql-file without it re-running scripts. It can be dropped by a
later sql-file-2 once rolling back past 0.6 is no longer a concern.sql-file-0 is still run (creating
sql_file_schema) for the same reason.sql-file-1 row is written last, so an interrupted migration is
simply repeated on the next open.open-local/open-pool take :schemas, a sequence of ids (strings)
or legacy [name n] vectors. These are the "targets", normally the
newest script of each chain the application uses.
requires. Scripts already
recorded as installed are not read for their requires: the stored
requires column is used instead, so deleted old scripts don't
break resolution.a -> b -> c -> a), an
invalid id, or a bad directive.Install in dependency order: a depth-first, post-order walk from the
targets, taking targets in the order given and each script's
requires in the order declared. Independent branches therefore
install in a predictable, repeatable order.
For each schema in that order that isn't installed:
replaces directives, apply the 0.5.0 rules,
adapted to ids (see below). If they say to record rather than run,
insert a recorded row and move on.how = 'run', its requires, replaces and
digest, started_on = now and completed_on = NULL.completed_on = now.If a statement fails, the row stays with completed_on NULL. HSQLDB
commits DDL immediately, so the database really is partly changed;
see Incomplete installs.
replaces with idsA script with replaces R (possibly several):
<name>-<n>), any installed <name>-<k> counts, so this works
even after the old chain's scripts are deleted.requires column this check
doesn't depend on old script files still existing.On open, any row with completed_on NULL means a script started and
didn't finish. Without statement tracking (phase 4), this is an error
naming the schema: the database is partly changed and needs restoring
(or fixing by hand) before continuing.
This replaces the "Cannot downgrade" check. Installed schemas that
aren't reachable from the targets (through requires and replaces)
usually mean old code running against a newer database, for example
after a rollback. That's an error in production and a warning in
development. sql-file's own schemas (sql-file-*) are exempt. The
check is made by open-local (which knows the whole request) before
anything is installed; ensure-schema, which installs a single schema,
doesn't make it.
The digest is a SHA-256 over a canonical encoding of what the script means:
requires, in order (for a legacy script, including
the implicit <name>-<n-1>),replaces,sql-statements produces it, in order.Each element is written as a one-byte tag (R, P, or S), its
length in UTF-8 bytes (four bytes, big-endian), and its bytes, so no
two different scripts encode the same way.
Because sql-statements collapses whitespace runs to a single space,
drops -- comments, and keeps string literals exactly, the digest
ignores comments, blank lines, and re-indentation or re-flowing of a
statement, but not other changes to it. Changing a dependency or a
replacement does change the digest.
On each open, for every installed schema with a digest whose script can still be found, recompute and compare:
migrated rows): store the current digest, logged
at INFO. The first open after upgrading sql-file adopts whatever the
scripts say at that point.sql-file-*) aren't checked; they change
only with sql-file itself.A new open option, :development-mode (default false), sets the
defaults for the behaviors that differ:
| production | development | |
|---|---|---|
| Changed installed script (digest mismatch) | error | warning |
| Installed schema not covered by the request | error | warning |
| Incomplete install | error | resume (phase 4), else error |
Each can be overridden individually
(:on-schema-change, :on-uncovered-schema, each :error or
:warn). ectcommon's start-app would pass its own
:development-mode setting through.
Enabled with :track-statements true, which defaults to the value of
:development-mode.
sql_file_statement is
written after it succeeds (digest of the statement text, as above).
HSQLDB commits DDL immediately, so the rows match the database.statements_tracked). An install that ran untracked can't be
resumed, even with tracking on now: there's no record of which
statements were applied, and a failure on the first statement looks
the same as no tracking at all.requires added while fixing it
are installed first. Its row's requires, replaces and digest are
updated to the fixed script when it completes.statements_tracked existed get the column added on open.open-local, open-pool, with-pool: :schemas accepts ids and
legacy vectors; new options as above.installed-schemas (the rows of sql_file_installed),
schema-installed?.ensure-schema takes an id (or legacy vector).get-schema-version / set-schema-version!: kept for legacy
vectors during the transition (reading sql_file_installed),
deprecated, removed with legacy requests.script-directives: returns {:requires [...] :replaces [...]}.script-digest, for tests and tooling.All of the phases below go into a single 0.6.0 release; they're
separate steps of the work, each with tests in the existing style
(small fixture scripts under test/resources, in-memory databases).
1.0.0 is a later decision.
sql-file-1, migration of
sql_file_schema, start/finish times, incomplete-install detection.
Existing numbered chains behave as before; replaces checks all
replaced schemas before running anything.requires,
resolution and topological install, legacy naming, replaces by
id, the uncovered-schema check, :development-mode.[name n] requests and
get/set-schema-version; drop sql_file_schema. Possibly 1.0.0.Test coverage to add along the way: diamond dependencies (installed
once, in the right order), cycles, missing dependencies, deterministic
order of independent branches, legacy and new ids mixed, migration of
an existing sql_file_schema, replaces across ids including the
"later" check without old script files, incomplete installs, digest
match/mismatch/adoption/missing script, and statement resume including
an edited applied statement.
Nothing has to change when they move to 0.6.0. Their
existing scripts (ectcommon-0, todo-0, stocking-5, the frozen
toto-*) keep their legacy ids. New scripts can use the new style,
for example ectcommon/0001-session-key with
requires ectcommon-0, and the applications switch their :schemas
from [["todo" 0]] to the newest id when they do.
The digest adoption step means the first open after upgrading records digests for scripts as they stand; edits to those scripts after that are detected.
requires closure be allowed to name schemas that
are only satisfied by replaces (a script required under its old
id, after a rename)? Current plan: no; require the new id.recorded rows should store the digest of the replacing
script.~~ Decided: yes, so later edits to a baseline are noticed.Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |