0.3

Every published OKModel release, newest first.

all series · 0.4 · 0.2

v0.3.0

0.3.0 adds extensions, domains, functions and triggers, views, and roles and grants, and checks that each one round-trips.

✨ Added

dialects

  • extension() from okmodel/ext declares an extension. schema({ extensions }) stores those objects and, when the catalog is built, asks each one for its record. citext() (okmodel/pg/citext) and pgTrgm() (okmodel/pg/pg_trgm) are the built-ins. pgTrgm.similar and pgTrgm.wordSimilar are where filters.
  • A second declaration of the same extension is OKM1813. A citext or ltree column whose extension is missing from a non-empty list is OKM1810. Omitting the list leaves the existing dependency check (OKM1020).
  • Migrations create an extension before the tables that use it, upgrade with ALTER EXTENSION … UPDATE (the step is path-unverified until apply checks pg_extension_update_paths), move a relocatable extension with SET SCHEMA, and drop with DROP EXTENSION and no CASCADE. A downgrade or a non-relocatable move is OKM1814. Apply checks pg_available_extensions before any statement (OKM1811). An unpinned declaration matches whatever version is installed.
  • pgTrgm.gin() and pgTrgm.gist() store a using gin or using gist index with gin_trgm_ops or gist_trgm_ops. The plan creates that index, and introspection of it matches the declaration.
  • t.domain(name, base, check) is a catalog type. The column's TypeScript type and codec are the base column's. Creating the domain, changing its check, and dropping it are planned. A check change is ALTER DOMAIN … ADD CONSTRAINT … NOT VALID, then VALIDATE CONSTRAINT, then a drop of the previous check, and never CASCADE. Changing the base type is OKM1020. An enum cannot be the base (OKM1060).
  • fn() and trigger() from okmodel/fn declare functions and triggers. schema({ functions, triggers }) stores them. A function's identity is schema, name, and argument types, so overloads are distinct. A trigger's identity is table and name. plpgsql without dependsOn is OKM1824. security: "definer" without searchPath is OKM1823.
  • A plan creates types and tables, then functions, then triggers. The same signature is CREATE OR REPLACE and the step is marked behavior: change. A return-type change drops dependents and recreates them, and never uses CASCADE. A drop goes in reverse and is refused while a dependent remains (OKM1821). Triggers are dropped before their functions.
  • timestamps({ enforce: "trigger" }) adds a before-update trigger and its function, with timestamps provenance. LANGUAGE sql that starts with BEGIN ATOMIC takes its dependencies from pg_depend.
  • view() and materializedView() from okmodel/view declare views. schema({ views }) stores them. A view is SQL with declared columns. Appending columns is CREATE OR REPLACE and the step is marked behavior: change. An incompatible replace, or a change to a column the view reads, drops the dependents and recreates them, and never uses CASCADE. A drop goes in reverse and is refused while a dependent remains (OKM1821).
  • A materialized view has no CREATE OR REPLACE. Creation is WITH NO DATA, and populate is a planned data step. refresh: "concurrently" needs a unique index (OKM1822). Scratch verification reads column dependencies from pg_depend and stores the server reprint of the query.
  • db.views.<name>.find(...) reads a view. Writes are not on the handle. A view that exposes the tenant key takes the tenant predicate. A view that reads a tenant table and does not expose the key is OKM1820 in okm check unless declared global("reason").
  • defineConfig({ roles: { migration, app } }) adds role, grant, and default-privilege objects. A name is external unless it is listed in managed. A managed role is created when pg_roles does not have it, altered in place, and never dropped. Grants and default privileges are for the application role on every managed table, view, materialized view, sequence, and function. Fine-grained grants stay in M2.

tooling

  • okm ext list prints the extensions the connected server can install and the version that is installed. okm ext check compares those versions with the schema. A missing extension is OKM1811. A pin the server does not meet is OKM1812. okm ext test and okm ext scaffold are not in this version.
  • okm doctor lists the triggers on each table. okm doctor OKMxxxx prints that code. When roles is set, doctor also checks that an external role exists, that a managed role can be created (CREATEROLE), and that the application role can reach each managed object (OKM1825).
  • The 0.3 gate pushes an extension, a domain, a function, a trigger from timestamps({ enforce: "trigger" }), a view, a materialized view, and roles onto an empty database, checks for no drift, alters each one, and removes it without CASCADE. An extension the server cannot install is OKM1811 before any statement.
  • Apply runs migrations as the migration role. When that role exists and is not current_user, the runner issues one SET ROLE before any statement and records it on the run report. The statement is not a plan step. CREATEROLE is checked before any statement when the plan creates or alters a role. CREATE ROLE has no IF NOT EXISTS; a role that already exists is skipped.

docs

  • Known limits: a typed call such as fn.slugify(col) is not in this version, inspect() does not list triggers, a plpgsql body is prosrc, and a BEGIN ATOMIC body is the server reprint.
  • Known limits: a view is SQL plus a declared column list, and db.views.<name>.find(...) is read-only. The query builder form (view(name, (q) => q.from(...))) is not in this version (D190). Column dependencies come from pg_depend. The stored query is the server reprint. refresh: "concurrently" is not stored by Postgres, and the plan's first populate is a plain REFRESH. security_invoker is not set.
  • Known limits for roles and grants: fine-grained grants are M2, a role is never dropped, OKM1825 is doctor only, and a function grant is name(argTypes). The non-owner RLS check moved to M2.
  • Known limits name this version. Typed fn calls, builder-defined views, okm ext test, okm ext scaffold, column-level grants, and row-level security stay unbuilt. A domain check or a BEGIN ATOMIC body that differs from the Postgres reprint only by parentheses or casts is still a plan change. okm check does not warn. Write the text Postgres prints.
  • The README names PostgreSQL 15 to 18, lists what 0.2 and 0.3 ship, and adds okm ext list, okm ext check, and okm doctor to the command table. The size rows are the bun run size output. The version line is 0.3.0 and the 0.3 roadmap item is done.

♻️ Changed

  • Startup gzip replaces lazy chunk ids with a fixed id of the same length before compressing, so a chunk whose only change is its content hash does not move the number.

🐛 Fixed

  • okm check reprints each view and materialized view with pg_get_viewdef on the connected server before it compares. A query Postgres 15 prints differently from Postgres 18 is not drift after push. A query that reprints to something else still is.
  • okm_meta and okm_history stay out of the author diff. Plan, push, check, and doctor neither create nor drop them, in either direction.
  • A primary key's name key is pkey in the schema and in introspection. CREATE TABLE writes that constraint's name, so a name other than {table}_pkey round-trips. After push, okm check reports no drift for a plain key, a composite key, and a named key. When okm_meta already exists, okm check compares the database with the schema and reports OKM1520 if they differ.
  • A plan treats a schema type spelling and the Postgres format_type spelling as the same type (D191). timestamptz matches timestamp with time zone, and the same for timestamp, time, timetz, varchar(n), char(n), and the integer, boolean, float, and decimal/numeric aliases. Length, precision, scale, and array ranks stay. An unknown name is compared as written. varchar(20) to varchar(30), and timestamp to timestamptz, still alter. After push, timestamps({ enforce: "trigger" }) plans to no steps.