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()fromokmodel/extdeclares an extension.schema({ extensions })stores those objects and, when the catalog is built, asks each one for its record.citext()(okmodel/pg/citext) andpgTrgm()(okmodel/pg/pg_trgm) are the built-ins.pgTrgm.similarandpgTrgm.wordSimilarare where filters.- A second declaration of the same extension is OKM1813. A
citextorltreecolumn 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 ispath-unverifieduntil apply checkspg_extension_update_paths), move a relocatable extension withSET SCHEMA, and drop withDROP EXTENSIONand noCASCADE. A downgrade or a non-relocatable move is OKM1814. Apply checkspg_available_extensionsbefore any statement (OKM1811). An unpinned declaration matches whatever version is installed. pgTrgm.gin()andpgTrgm.gist()store ausing ginorusing gistindex withgin_trgm_opsorgist_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 isALTER DOMAIN … ADD CONSTRAINT … NOT VALID, thenVALIDATE CONSTRAINT, then a drop of the previous check, and neverCASCADE. Changing the base type is OKM1020. An enum cannot be the base (OKM1060).fn()andtrigger()fromokmodel/fndeclare 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 withoutdependsOnis OKM1824.security: "definer"withoutsearchPathis OKM1823.- A plan creates types and tables, then functions, then triggers. The same signature is
CREATE OR REPLACEand the step is markedbehavior: change. A return-type change drops dependents and recreates them, and never usesCASCADE. 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 sqlthat starts withBEGIN ATOMICtakes its dependencies frompg_depend.view()andmaterializedView()fromokmodel/viewdeclare views.schema({ views })stores them. A view is SQL with declared columns. Appending columns isCREATE OR REPLACEand the step is markedbehavior: change. An incompatible replace, or a change to a column the view reads, drops the dependents and recreates them, and never usesCASCADE. A drop goes in reverse and is refused while a dependent remains (OKM1821).- A materialized view has no
CREATE OR REPLACE. Creation isWITH NO DATA, and populate is a planned data step.refresh: "concurrently"needs a unique index (OKM1822). Scratch verification reads column dependencies frompg_dependand 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 inokm checkunless declaredglobal("reason").defineConfig({ roles: { migration, app } })adds role, grant, and default-privilege objects. A name is external unless it is listed inmanaged. A managed role is created whenpg_rolesdoes 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 listprints the extensions the connected server can install and the version that is installed.okm ext checkcompares those versions with the schema. A missing extension is OKM1811. A pin the server does not meet is OKM1812.okm ext testandokm ext scaffoldare not in this version.okm doctorlists the triggers on each table.okm doctor OKMxxxxprints that code. Whenrolesis 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 withoutCASCADE. 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 oneSET ROLEbefore any statement and records it on the run report. The statement is not a plan step.CREATEROLEis checked before any statement when the plan creates or alters a role.CREATE ROLEhas noIF 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 isprosrc, and aBEGIN ATOMICbody 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 frompg_depend. The stored query is the server reprint.refresh: "concurrently"is not stored by Postgres, and the plan's first populate is a plainREFRESH.security_invokeris 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
fncalls, builder-defined views,okm ext test,okm ext scaffold, column-level grants, and row-level security stay unbuilt. A domain check or aBEGIN ATOMICbody that differs from the Postgres reprint only by parentheses or casts is still a plan change.okm checkdoes 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, andokm doctorto the command table. The size rows are thebun run sizeoutput. 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 checkreprints each view and materialized view withpg_get_viewdefon 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_metaandokm_historystay 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
pkeyin the schema and in introspection.CREATE TABLEwrites that constraint's name, so a name other than{table}_pkeyround-trips. After push,okm checkreports no drift for a plain key, a composite key, and a named key. Whenokm_metaalready exists,okm checkcompares the database with the schema and reports OKM1520 if they differ. - A plan treats a schema type spelling and the Postgres
format_typespelling as the same type (D191).timestamptzmatchestimestamp with time zone, and the same fortimestamp,time,timetz,varchar(n),char(n), and the integer, boolean, float, anddecimal/numericaliases. Length, precision, scale, and array ranks stay. An unknown name is compared as written.varchar(20)tovarchar(30), andtimestamptotimestamptz, still alter. After push,timestamps({ enforce: "trigger" })plans to no steps.