diff --git a/lerna.json b/lerna.json index 1a5954c68..949c96298 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "0.36.0", + "version": "0.39.0", "npmClient": "pnpm" } diff --git a/package.json b/package.json index 99a87a5a9..0eadb8e6f 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "jest": "^30.2.0", "jest-in-case": "^1.0.2", "lerna": "^8.2.3", - "pgsql-test": "^5.4.1", + "pgsql-test": "^5.10.6", "pnpm-policy": "^0.2.2", "prettier": "^3.0.2", "rimraf": "4.4.1", diff --git a/packages/achievements/Makefile b/packages/achievements/Makefile index e8614d648..2e78b8e4f 100644 --- a/packages/achievements/Makefile +++ b/packages/achievements/Makefile @@ -1,5 +1,5 @@ EXTENSION = pgpm-achievements -DATA = sql/pgpm-achievements--0.36.0.sql +DATA = sql/pgpm-achievements--0.39.0.sql PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) diff --git a/packages/achievements/package.json b/packages/achievements/package.json index b9a4df7b0..344456857 100644 --- a/packages/achievements/package.json +++ b/packages/achievements/package.json @@ -1,6 +1,6 @@ { "name": "@pgpm/achievements", - "version": "0.36.0", + "version": "0.39.0", "description": "Achievement system for tracking user progress and milestones", "author": "Dan Lynch ", "contributors": [ diff --git a/packages/achievements/pgpm-achievements.control b/packages/achievements/pgpm-achievements.control index 478235c93..b848bc530 100644 --- a/packages/achievements/pgpm-achievements.control +++ b/packages/achievements/pgpm-achievements.control @@ -1,6 +1,6 @@ # pgpm-achievements extension comment = 'pgpm-achievements extension' -default_version = '0.36.0' +default_version = '0.39.0' module_pathname = '$libdir/pgpm-achievements' requires = 'plpgsql,pgpm-jwt-claims,pgpm-verify' relocatable = false diff --git a/packages/achievements/revert/schemas/status_private/procedures/status_triggers.sql b/packages/achievements/revert/schemas/status_private/procedures/status_triggers.sql index d6366ad92..ff205fdc3 100644 --- a/packages/achievements/revert/schemas/status_private/procedures/status_triggers.sql +++ b/packages/achievements/revert/schemas/status_private/procedures/status_triggers.sql @@ -2,9 +2,9 @@ BEGIN; -DROP FUNCTION status_private.tg_achievement; -DROP FUNCTION status_private.tg_achievement_toggle; -DROP FUNCTION status_private.tg_achievement_boolean; -DROP FUNCTION status_private.tg_achievement_toggle_boolean; +DROP FUNCTION status_private.tg_achievement_toggle_boolean(); +DROP FUNCTION status_private.tg_achievement_boolean(); +DROP FUNCTION status_private.tg_achievement_toggle(); +DROP FUNCTION status_private.tg_achievement(); COMMIT; diff --git a/packages/achievements/revert/schemas/status_private/procedures/upsert_achievement.sql b/packages/achievements/revert/schemas/status_private/procedures/upsert_achievement.sql index defa0d47e..6a1d35cf5 100644 --- a/packages/achievements/revert/schemas/status_private/procedures/upsert_achievement.sql +++ b/packages/achievements/revert/schemas/status_private/procedures/upsert_achievement.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION status_private.upsert_achievement; +DROP FUNCTION status_private.upsert_achievement(uuid, text, int4); COMMIT; diff --git a/packages/achievements/revert/schemas/status_private/procedures/user_completed_step.sql b/packages/achievements/revert/schemas/status_private/procedures/user_completed_step.sql index 491e82095..9bb0d7aae 100644 --- a/packages/achievements/revert/schemas/status_private/procedures/user_completed_step.sql +++ b/packages/achievements/revert/schemas/status_private/procedures/user_completed_step.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION status_private.user_completed_step; +DROP FUNCTION status_private.user_completed_step(text, uuid); COMMIT; diff --git a/packages/achievements/revert/schemas/status_private/procedures/user_incompleted_step.sql b/packages/achievements/revert/schemas/status_private/procedures/user_incompleted_step.sql index 937e1b177..0247d52a1 100644 --- a/packages/achievements/revert/schemas/status_private/procedures/user_incompleted_step.sql +++ b/packages/achievements/revert/schemas/status_private/procedures/user_incompleted_step.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION status_private.user_incompleted_step; +DROP FUNCTION status_private.user_incompleted_step(text, uuid); COMMIT; diff --git a/packages/achievements/revert/schemas/status_public/procedures/steps_required.sql b/packages/achievements/revert/schemas/status_public/procedures/steps_required.sql index 5df0166d4..6bf6e1307 100644 --- a/packages/achievements/revert/schemas/status_public/procedures/steps_required.sql +++ b/packages/achievements/revert/schemas/status_public/procedures/steps_required.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION status_public.steps_required; +DROP FUNCTION status_public.steps_required(text, uuid); COMMIT; diff --git a/packages/achievements/revert/schemas/status_public/procedures/user_achieved.sql b/packages/achievements/revert/schemas/status_public/procedures/user_achieved.sql index b10e21011..1c07742ca 100644 --- a/packages/achievements/revert/schemas/status_public/procedures/user_achieved.sql +++ b/packages/achievements/revert/schemas/status_public/procedures/user_achieved.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION status_public.user_achieved; +DROP FUNCTION status_public.user_achieved(text, uuid); COMMIT; diff --git a/packages/achievements/revert/schemas/status_public/tables/user_steps/triggers/update_achievements_tg.sql b/packages/achievements/revert/schemas/status_public/tables/user_steps/triggers/update_achievements_tg.sql index bd79742dd..5ff776622 100644 --- a/packages/achievements/revert/schemas/status_public/tables/user_steps/triggers/update_achievements_tg.sql +++ b/packages/achievements/revert/schemas/status_public/tables/user_steps/triggers/update_achievements_tg.sql @@ -3,6 +3,6 @@ BEGIN; DROP TRIGGER update_achievements_tg ON status_public.user_steps; -DROP FUNCTION status_private.tg_update_achievements_tg; +DROP FUNCTION status_private.tg_update_achievements_tg(); COMMIT; diff --git a/packages/achievements/sql/pgpm-achievements--0.36.0.bundle.tar.gz b/packages/achievements/sql/pgpm-achievements--0.36.0.bundle.tar.gz deleted file mode 100644 index e2d772c22..000000000 Binary files a/packages/achievements/sql/pgpm-achievements--0.36.0.bundle.tar.gz and /dev/null differ diff --git a/packages/achievements/sql/pgpm-achievements--0.39.0.bundle.tar.gz b/packages/achievements/sql/pgpm-achievements--0.39.0.bundle.tar.gz new file mode 100644 index 000000000..9c3505ba5 Binary files /dev/null and b/packages/achievements/sql/pgpm-achievements--0.39.0.bundle.tar.gz differ diff --git a/packages/achievements/sql/pgpm-achievements--0.36.0.sql b/packages/achievements/sql/pgpm-achievements--0.39.0.sql similarity index 100% rename from packages/achievements/sql/pgpm-achievements--0.36.0.sql rename to packages/achievements/sql/pgpm-achievements--0.39.0.sql diff --git a/packages/achievements/verify/schemas/status_private/procedures/status_triggers.sql b/packages/achievements/verify/schemas/status_private/procedures/status_triggers.sql index 37cd8e604..ec687d19c 100644 --- a/packages/achievements/verify/schemas/status_private/procedures/status_triggers.sql +++ b/packages/achievements/verify/schemas/status_private/procedures/status_triggers.sql @@ -2,9 +2,9 @@ BEGIN; -SELECT verify_function ('status_private.tg_achievement'); -SELECT verify_function ('status_private.tg_achievement_toggle'); -SELECT verify_function ('status_private.tg_achievement_boolean'); -SELECT verify_function ('status_private.tg_achievement_toggle_boolean'); +SELECT assert_function('status_private.tg_achievement()'::regprocedure); +SELECT assert_function('status_private.tg_achievement_toggle()'::regprocedure); +SELECT assert_function('status_private.tg_achievement_boolean()'::regprocedure); +SELECT assert_function('status_private.tg_achievement_toggle_boolean()'::regprocedure); ROLLBACK; diff --git a/packages/achievements/verify/schemas/status_private/procedures/upsert_achievement.sql b/packages/achievements/verify/schemas/status_private/procedures/upsert_achievement.sql index 3ba835177..8d25293f5 100644 --- a/packages/achievements/verify/schemas/status_private/procedures/upsert_achievement.sql +++ b/packages/achievements/verify/schemas/status_private/procedures/upsert_achievement.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('status_private.upsert_achievement'); +SELECT assert_function('status_private.upsert_achievement(uuid, text, int4)'::regprocedure); ROLLBACK; diff --git a/packages/achievements/verify/schemas/status_private/procedures/user_completed_step.sql b/packages/achievements/verify/schemas/status_private/procedures/user_completed_step.sql index a230f1760..04d5a0223 100644 --- a/packages/achievements/verify/schemas/status_private/procedures/user_completed_step.sql +++ b/packages/achievements/verify/schemas/status_private/procedures/user_completed_step.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('status_private.user_completed_step'); +SELECT assert_function('status_private.user_completed_step(text, uuid)'::regprocedure); ROLLBACK; diff --git a/packages/achievements/verify/schemas/status_private/procedures/user_incompleted_step.sql b/packages/achievements/verify/schemas/status_private/procedures/user_incompleted_step.sql index 3a23e74db..5bf9e9305 100644 --- a/packages/achievements/verify/schemas/status_private/procedures/user_incompleted_step.sql +++ b/packages/achievements/verify/schemas/status_private/procedures/user_incompleted_step.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('status_private.user_incompleted_step'); +SELECT assert_function('status_private.user_incompleted_step(text, uuid)'::regprocedure); ROLLBACK; diff --git a/packages/achievements/verify/schemas/status_private/schema.sql b/packages/achievements/verify/schemas/status_private/schema.sql index 82af3751a..e7416451d 100644 --- a/packages/achievements/verify/schemas/status_private/schema.sql +++ b/packages/achievements/verify/schemas/status_private/schema.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_schema ('status_private'); +SELECT assert_schema('status_private'::regnamespace); ROLLBACK; diff --git a/packages/achievements/verify/schemas/status_public/procedures/steps_required.sql b/packages/achievements/verify/schemas/status_public/procedures/steps_required.sql index 9387279d2..acd3130cb 100644 --- a/packages/achievements/verify/schemas/status_public/procedures/steps_required.sql +++ b/packages/achievements/verify/schemas/status_public/procedures/steps_required.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('status_public.steps_required'); +SELECT assert_function('status_public.steps_required(text, uuid)'::regprocedure); ROLLBACK; diff --git a/packages/achievements/verify/schemas/status_public/procedures/user_achieved.sql b/packages/achievements/verify/schemas/status_public/procedures/user_achieved.sql index 8d1733aa6..840d1bdc7 100644 --- a/packages/achievements/verify/schemas/status_public/procedures/user_achieved.sql +++ b/packages/achievements/verify/schemas/status_public/procedures/user_achieved.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('status_public.user_achieved'); +SELECT assert_function('status_public.user_achieved(text, uuid)'::regprocedure); ROLLBACK; diff --git a/packages/achievements/verify/schemas/status_public/schema.sql b/packages/achievements/verify/schemas/status_public/schema.sql index 6879a3be8..3ed770ddc 100644 --- a/packages/achievements/verify/schemas/status_public/schema.sql +++ b/packages/achievements/verify/schemas/status_public/schema.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_schema ('status_public'); +SELECT assert_schema('status_public'::regnamespace); ROLLBACK; diff --git a/packages/achievements/verify/schemas/status_public/tables/level_requirements/table.sql b/packages/achievements/verify/schemas/status_public/tables/level_requirements/table.sql index b19ac26dd..87351102d 100644 --- a/packages/achievements/verify/schemas/status_public/tables/level_requirements/table.sql +++ b/packages/achievements/verify/schemas/status_public/tables/level_requirements/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('status_public.level_requirements'); +SELECT assert_table('status_public.level_requirements'::regclass); ROLLBACK; diff --git a/packages/achievements/verify/schemas/status_public/tables/levels/table.sql b/packages/achievements/verify/schemas/status_public/tables/levels/table.sql index ccf3db62d..0cda8c3b7 100644 --- a/packages/achievements/verify/schemas/status_public/tables/levels/table.sql +++ b/packages/achievements/verify/schemas/status_public/tables/levels/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('status_public.levels'); +SELECT assert_table('status_public.levels'::regclass); ROLLBACK; diff --git a/packages/achievements/verify/schemas/status_public/tables/user_achievements/policies/enable_row_level_security.sql b/packages/achievements/verify/schemas/status_public/tables/user_achievements/policies/enable_row_level_security.sql index f85f6918b..7c71e6d02 100644 --- a/packages/achievements/verify/schemas/status_public/tables/user_achievements/policies/enable_row_level_security.sql +++ b/packages/achievements/verify/schemas/status_public/tables/user_achievements/policies/enable_row_level_security.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_security ('status_public.user_achievements'); +SELECT assert_table_security('status_public.user_achievements'::regclass); ROLLBACK; diff --git a/packages/achievements/verify/schemas/status_public/tables/user_achievements/policies/user_achievements_policy.sql b/packages/achievements/verify/schemas/status_public/tables/user_achievements/policies/user_achievements_policy.sql index 6c4e11cb4..ef17c9eb3 100644 --- a/packages/achievements/verify/schemas/status_public/tables/user_achievements/policies/user_achievements_policy.sql +++ b/packages/achievements/verify/schemas/status_public/tables/user_achievements/policies/user_achievements_policy.sql @@ -2,14 +2,14 @@ BEGIN; -SELECT verify_policy ('can_select_user_achievements', 'status_public.user_achievements'); -SELECT verify_policy ('can_insert_user_achievements', 'status_public.user_achievements'); -SELECT verify_policy ('can_update_user_achievements', 'status_public.user_achievements'); -SELECT verify_policy ('can_delete_user_achievements', 'status_public.user_achievements'); - -SELECT has_table_privilege('authenticated', 'status_public.user_achievements', 'INSERT'); -SELECT has_table_privilege('authenticated', 'status_public.user_achievements', 'SELECT'); -SELECT has_table_privilege('authenticated', 'status_public.user_achievements', 'UPDATE'); -SELECT has_table_privilege('authenticated', 'status_public.user_achievements', 'DELETE'); +SELECT assert_policy('status_public.user_achievements'::regclass, 'can_select_user_achievements', 'select', true, true, false); +SELECT assert_policy('status_public.user_achievements'::regclass, 'can_insert_user_achievements', 'insert', true, false, true); +SELECT assert_policy('status_public.user_achievements'::regclass, 'can_update_user_achievements', 'update', true, true, false); +SELECT assert_policy('status_public.user_achievements'::regclass, 'can_delete_user_achievements', 'delete', true, true, false); + +SELECT assert_table_grant('status_public.user_achievements'::regclass, 'authenticated', 'INSERT'); +SELECT assert_table_grant('status_public.user_achievements'::regclass, 'authenticated', 'SELECT'); +SELECT assert_table_grant('status_public.user_achievements'::regclass, 'authenticated', 'UPDATE'); +SELECT assert_table_grant('status_public.user_achievements'::regclass, 'authenticated', 'DELETE'); ROLLBACK; diff --git a/packages/achievements/verify/schemas/status_public/tables/user_achievements/table.sql b/packages/achievements/verify/schemas/status_public/tables/user_achievements/table.sql index 0de36f430..4e72dac81 100644 --- a/packages/achievements/verify/schemas/status_public/tables/user_achievements/table.sql +++ b/packages/achievements/verify/schemas/status_public/tables/user_achievements/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('status_public.user_achievements'); +SELECT assert_table('status_public.user_achievements'::regclass); ROLLBACK; diff --git a/packages/achievements/verify/schemas/status_public/tables/user_steps/table.sql b/packages/achievements/verify/schemas/status_public/tables/user_steps/table.sql index 934c3cddf..16755b3c0 100644 --- a/packages/achievements/verify/schemas/status_public/tables/user_steps/table.sql +++ b/packages/achievements/verify/schemas/status_public/tables/user_steps/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('status_public.user_steps'); +SELECT assert_table('status_public.user_steps'::regclass); ROLLBACK; diff --git a/packages/achievements/verify/schemas/status_public/tables/user_steps/triggers/update_achievements_tg.sql b/packages/achievements/verify/schemas/status_public/tables/user_steps/triggers/update_achievements_tg.sql index fdfc82b8c..a1e47a909 100644 --- a/packages/achievements/verify/schemas/status_public/tables/user_steps/triggers/update_achievements_tg.sql +++ b/packages/achievements/verify/schemas/status_public/tables/user_steps/triggers/update_achievements_tg.sql @@ -2,7 +2,7 @@ BEGIN; -SELECT verify_function ('status_private.tg_update_achievements_tg'); -SELECT verify_trigger ('status_public.update_achievements_tg'); +SELECT assert_function('status_private.tg_update_achievements_tg()'::regprocedure); +SELECT assert_trigger('status_public.user_steps'::regclass, 'update_achievements_tg', 'status_private.tg_update_achievements_tg'::regproc, 5); ROLLBACK; diff --git a/packages/app-scope/Makefile b/packages/app-scope/Makefile index 568fbcd45..967878598 100644 --- a/packages/app-scope/Makefile +++ b/packages/app-scope/Makefile @@ -1,5 +1,5 @@ EXTENSION = pgpm-app-scope -DATA = sql/pgpm-app-scope--0.36.0.sql +DATA = sql/pgpm-app-scope--0.39.0.sql PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) diff --git a/packages/app-scope/__tests__/membership.test.ts b/packages/app-scope/__tests__/membership.test.ts index 9d8ad8f83..1e6f494a9 100644 --- a/packages/app-scope/__tests__/membership.test.ts +++ b/packages/app-scope/__tests__/membership.test.ts @@ -172,7 +172,7 @@ describe('app_scope scope frames (membership climb + platform fall-through)', () ]); }); - it('frames(): custom entity climb then full platform fall-through (8 frames)', async () => { + it('frames(): custom entity climb then full platform fall-through (9 frames)', async () => { const rows = await pg.any( `SELECT scope, lookup_database_id, key_value FROM app_scope.frames($1, 'team', $2)`, @@ -184,6 +184,10 @@ describe('app_scope scope frames (membership climb + platform fall-through)', () { scope: 'department', lookup_database_id: TENANT_DB, key_value: DEPT_ID }, { scope: 'org', lookup_database_id: TENANT_DB, key_value: ORG_ID }, { scope: 'app', lookup_database_id: TENANT_DB, key_value: null }, + // the tenant's OWN database frame precedes the platform fall-through, so a + // surface the tenant provisioned for itself outranks the shared platform + // plane the fall-through serves for the same tenant key. + { scope: 'database', lookup_database_id: TENANT_DB, key_value: TENANT_DB }, // platform database full chain, then the global platform terminal. // The fall-through database frame is re-keyed by the EXECUTION database: // rows the tenant sees on this shared plane are keyed by the tenant's id. diff --git a/packages/app-scope/deploy/schemas/app_scope/procedures/frames.sql b/packages/app-scope/deploy/schemas/app_scope/procedures/frames.sql index d3f6b0572..45c3eebd2 100644 --- a/packages/app-scope/deploy/schemas/app_scope/procedures/frames.sql +++ b/packages/app-scope/deploy/schemas/app_scope/procedures/frames.sql @@ -22,7 +22,12 @@ BEGIN; -- tenant custom entity execution (e.g. `team` owned by `department` owned by -- an org): -- team -> department -> org -> app (in the tenant database) +-- -> database (the tenant's own database frame) -- -> database -> org -> app -> platform (in the platform database) +-- A non-`database` tenant execution inserts the tenant's own `database` frame +-- before the platform fall-through, so a surface the tenant provisioned for +-- itself outranks the shared platform plane the fall-through serves for that +-- same tenant key. A `database` execution already emits it in its local chain. -- platform database execution (any scope): -- -> platform -- `platform` execution scope (anywhere): @@ -89,6 +94,22 @@ BEGIN RETURN; END IF; + -- Before falling through to the platform's shared planes, a tenant execution + -- resolves the execution database's OWN `database` frame: a surface the + -- tenant has provisioned for itself must answer ahead of the shared platform + -- plane the fall-through would otherwise serve for that same tenant key. The + -- fall-through already re-keys the platform `database` frame by this tenant + -- (below), so both frames name the identity (database, this tenant); the + -- tenant's own is strictly more specific and comes first. Skipped for a + -- `database` execution (its local chain already emitted this frame) and for + -- the platform database itself (which has no tenant fall-through). + IF NOT v_is_platform_db AND frames.execution_scope <> 'database' THEN + scope := 'database'; + lookup_database_id := frames.database_id; + key_value := frames.database_id; + RETURN NEXT; + END IF; + -- Fall through to the platform database's OWN full local chain (its -- database -> org -> app), unless the execution already ran inside the -- platform database (in which case its local chain above already covered it). diff --git a/packages/app-scope/deploy/schemas/app_scope/procedures/membership_parent.sql b/packages/app-scope/deploy/schemas/app_scope/procedures/membership_parent.sql index ac5aedb9c..0b3471a81 100644 --- a/packages/app-scope/deploy/schemas/app_scope/procedures/membership_parent.sql +++ b/packages/app-scope/deploy/schemas/app_scope/procedures/membership_parent.sql @@ -1,5 +1,6 @@ -- Deploy schemas/app_scope/procedures/membership_parent to pg -- requires: schemas/app_scope/schema +-- requires: schemas/app_scope/procedures/projected_parent -- requires: metaschema-schema:schemas/metaschema_public/tables/table/table -- requires: metaschema-modules:schemas/metaschema_modules_public/tables/memberships_module/table -- requires: metaschema-modules:schemas/metaschema_modules_public/tables/membership_types_module/table @@ -10,14 +11,27 @@ BEGIN; -- scope to climb to, and the runtime entity table + owner FK column used to walk -- to the parent row. -- --- The runtime membership_types table (resolved per database) supplies the type --- ints; metaschema_modules_public.memberships_module supplies the entity table --- and owner field. Returns no row when the scope is not a membership scope +-- The type ints come from app_scope.projected_parent — one static query over the +-- published scope type projection — and fall back to probing the runtime +-- membership_types table only when the projection cannot answer. +-- metaschema_modules_public.memberships_module supplies the entity table and owner +-- field either way. Returns no row when the scope is not a membership scope -- (e.g. `app`), signalling the caller to stop the membership walk. -- --- The per-database membership_types probe is a dynamic SELECT against a --- dynamically-named table, built with format()/quote_ident + EXECUTE ... USING --- (identifier is data, values are bound params). No AST/deparser dependency. +-- The projection answers in a database deployed from the published platform +-- modules, where its schema carries the stable name. The fallback covers the two +-- places that name cannot exist: a live-provisioned platform database, whose +-- planes sit under the database's schema hash until an export stabilises them, +-- and a database with no scope plane installed. That probe is a dynamic SELECT +-- against a dynamically-named table, built with format()/quote_ident + +-- EXECUTE ... USING (identifier is data, values are bound params) — no +-- AST/deparser dependency, so it stays portable into any provisioned database. +-- +-- The two paths must answer identically or the climb is wrong, so they are +-- asserted against each other in +-- packages/metaschema/__tests__/modules/scope-types.test.ts. The dynamic half is +-- deleted once every environment the climb runs in deploys the published module — +-- see docs/architecture/platform-publication-pipeline.md. CREATE FUNCTION app_scope.membership_parent( database_id uuid, scope text @@ -39,45 +53,56 @@ DECLARE v_entity_table_owner_id uuid; v_query text; BEGIN - SELECT mtm.table_id - INTO v_types_table_id - FROM metaschema_modules_public.membership_types_module mtm - WHERE mtm.database_id = membership_parent.database_id; - - IF v_types_table_id IS NULL THEN - RETURN; - END IF; + -- Projection first: both hops in one static query, and no metadata lookups to + -- locate a hash-named table. + SELECT pp.membership_type, pp.parent_scope + INTO v_membership_type, v_parent_scope + FROM app_scope.projected_parent( + membership_parent.database_id, + membership_parent.scope + ) pp; - -- Locate the physical membership_types table (inline schema_and_table). - SELECT s.schema_name, t.name - INTO v_types_schema, v_types_table - FROM metaschema_public.schema s - JOIN metaschema_public."table" t ON (t.schema_id = s.id AND t.database_id = s.database_id) - WHERE t.id = v_types_table_id; + IF v_membership_type IS NULL THEN + SELECT mtm.table_id + INTO v_types_table_id + FROM metaschema_modules_public.membership_types_module mtm + WHERE mtm.database_id = membership_parent.database_id; - IF NOT FOUND THEN - RETURN; - END IF; + IF v_types_table_id IS NULL THEN + RETURN; + END IF; - -- SELECT id, parent_membership_type FROM "" WHERE scope = $1 - v_query := format( - 'SELECT id, parent_membership_type FROM %I.%I WHERE scope = $1', - v_types_schema, v_types_table - ); - EXECUTE v_query INTO v_membership_type, v_parent_membership_type USING membership_parent.scope; + -- Locate the physical membership_types table (inline schema_and_table). + SELECT s.schema_name, t.name + INTO v_types_schema, v_types_table + FROM metaschema_public.schema s + JOIN metaschema_public."table" t ON (t.schema_id = s.id AND t.database_id = s.database_id) + WHERE t.id = v_types_table_id; - IF v_membership_type IS NULL THEN - RETURN; - END IF; + IF NOT FOUND THEN + RETURN; + END IF; - -- Resolve the parent scope name (custom/org/app parents alike). - IF v_parent_membership_type IS NOT NULL THEN - -- SELECT scope FROM "" WHERE id = $1 + -- SELECT id, parent_membership_type FROM "" WHERE scope = $1 v_query := format( - 'SELECT scope FROM %I.%I WHERE id = $1', + 'SELECT id, parent_membership_type FROM %I.%I WHERE scope = $1', v_types_schema, v_types_table ); - EXECUTE v_query INTO v_parent_scope USING v_parent_membership_type; + EXECUTE v_query INTO v_membership_type, v_parent_membership_type USING membership_parent.scope; + + IF v_membership_type IS NULL THEN + RETURN; + END IF; + + -- Resolve the parent scope name (custom/org/app parents alike). + IF v_parent_membership_type IS NOT NULL THEN + -- SELECT scope FROM "" WHERE id = $1 + v_query := format( + 'SELECT scope FROM %I.%I WHERE id = $1', + v_types_schema, v_types_table + ); + EXECUTE v_query INTO v_parent_scope USING v_parent_membership_type; + END IF; END IF; -- Entity table + owner FK for the current scope (static metaschema config). diff --git a/packages/app-scope/deploy/schemas/app_scope/procedures/projected_parent.sql b/packages/app-scope/deploy/schemas/app_scope/procedures/projected_parent.sql new file mode 100644 index 000000000..17377076a --- /dev/null +++ b/packages/app-scope/deploy/schemas/app_scope/procedures/projected_parent.sql @@ -0,0 +1,56 @@ +-- Deploy schemas/app_scope/procedures/projected_parent to pg +-- requires: schemas/app_scope/schema + +BEGIN; + +-- projected_parent: one scope's membership type and the scope it climbs to, read +-- from the scope type projection with plain static SQL. +-- +-- This is the fast half of app_scope.membership_parent. The projection +-- (scope_private.scope_types, one row per scope of every logical database in this +-- Postgres database, keyed by database_id) holds exactly the type-level structure +-- the climb needs — `team` sits under `department` — so the two hops that +-- membership_parent resolves by probing a per-database, hash-named +-- membership_types table with EXECUTE become a single self-join here. +-- +-- Returns no row when the projection cannot answer, which is not an error and has +-- three ordinary causes: the plane is not installed in this database at all; the +-- schema exists under a database's hash rather than the published name (a +-- live-provisioned platform database, where names are only stabilised by export — +-- see docs/architecture/platform-publication-pipeline.md); or the scope is simply +-- not a membership scope. The caller falls back to the dynamic probe, so a +-- no-answer costs a to_regclass and nothing else. +-- +-- The literal name is the point: naming the published schema is what makes the +-- climb plannable, and it is checked at runtime rather than required at deploy +-- time so that the bootstrap and test-harness databases — which provision through +-- live triggers instead of deploying the published module, and which are what +-- build the published module in the first place — keep working unchanged. +CREATE FUNCTION app_scope.projected_parent( + database_id uuid, + scope text +) RETURNS TABLE ( + membership_type int, + parent_scope text +) AS $$ +BEGIN + IF to_regclass('scope_private.scope_types') IS NULL THEN + RETURN; + END IF; + + RETURN QUERY + SELECT st.membership_type, + parent.scope + FROM scope_private.scope_types st + LEFT JOIN scope_private.scope_types parent + ON parent.database_id = st.database_id + AND parent.membership_type = st.parent_membership_type + WHERE st.database_id = projected_parent.database_id + AND st.scope = projected_parent.scope; +END; +$$ LANGUAGE plpgsql STABLE SECURITY DEFINER; + +COMMENT ON FUNCTION app_scope.projected_parent(uuid, text) IS +'One scope''s membership type and parent scope, read from scope_private.scope_types with static SQL. Returns no row when the projection is absent or does not cover the scope, so callers fall back to the dynamic probe.'; + +COMMIT; diff --git a/packages/app-scope/package.json b/packages/app-scope/package.json index 7a54e4f5d..787737836 100644 --- a/packages/app-scope/package.json +++ b/packages/app-scope/package.json @@ -1,6 +1,6 @@ { "name": "@pgpm/app-scope", - "version": "0.36.0", + "version": "0.39.0", "description": "Portable scope-chain resolution primitive (ordered scope frames + platform database lookup)", "author": "Dan Lynch ", "contributors": [ diff --git a/packages/app-scope/pgpm-app-scope.control b/packages/app-scope/pgpm-app-scope.control index a1b7f39ba..52336d0a6 100644 --- a/packages/app-scope/pgpm-app-scope.control +++ b/packages/app-scope/pgpm-app-scope.control @@ -1,6 +1,6 @@ # pgpm-app-scope extension comment = 'pgpm-app-scope extension' -default_version = '0.36.0' +default_version = '0.39.0' module_pathname = '$libdir/pgpm-app-scope' requires = 'plpgsql,pgpm-verify,metaschema-schema,metaschema-modules' relocatable = false diff --git a/packages/app-scope/pgpm.plan b/packages/app-scope/pgpm.plan index e8893f017..580ac0498 100644 --- a/packages/app-scope/pgpm.plan +++ b/packages/app-scope/pgpm.plan @@ -5,7 +5,8 @@ schemas/app_scope/schema 2017-08-11T08:11:51Z constructive # app_scope schema schemas/app_scope/procedures/platform_database_id [schemas/app_scope/schema] 2017-08-11T08:11:51Z constructive # platform db id schemas/app_scope/procedures/dyn_lookup_uuid [schemas/app_scope/schema] 2017-08-11T08:11:51Z constructive # dyn lookup uuid -schemas/app_scope/procedures/membership_parent [schemas/app_scope/schema] 2017-08-11T08:11:51Z constructive # membership parent +schemas/app_scope/procedures/projected_parent [schemas/app_scope/schema] 2026-08-05T09:00:00Z devin # scope type projection parent lookup +schemas/app_scope/procedures/membership_parent [schemas/app_scope/schema schemas/app_scope/procedures/projected_parent] 2017-08-11T08:11:51Z constructive # membership parent schemas/app_scope/procedures/local_frames [schemas/app_scope/schema schemas/app_scope/procedures/dyn_lookup_uuid schemas/app_scope/procedures/membership_parent] 2017-08-11T08:11:51Z constructive # per-database local frames schemas/app_scope/procedures/frames [schemas/app_scope/schema schemas/app_scope/procedures/platform_database_id schemas/app_scope/procedures/local_frames] 2017-08-11T08:11:51Z constructive # ordered scope frames schemas/app_scope/procedures/routing_tables [schemas/app_scope/schema schemas/app_scope/procedures/frames] 2026-07-26T09:00:00Z devin # scoped routing/site/domain/route/app table name resolution diff --git a/packages/app-scope/revert/schemas/app_scope/procedures/dyn_lookup_uuid.sql b/packages/app-scope/revert/schemas/app_scope/procedures/dyn_lookup_uuid.sql index fa7bd6bc9..1ccd69a3f 100644 --- a/packages/app-scope/revert/schemas/app_scope/procedures/dyn_lookup_uuid.sql +++ b/packages/app-scope/revert/schemas/app_scope/procedures/dyn_lookup_uuid.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION app_scope.dyn_lookup_uuid; +DROP FUNCTION app_scope.dyn_lookup_uuid(text, text, text, uuid); COMMIT; diff --git a/packages/app-scope/revert/schemas/app_scope/procedures/frames.sql b/packages/app-scope/revert/schemas/app_scope/procedures/frames.sql index 66fbab431..a589597b3 100644 --- a/packages/app-scope/revert/schemas/app_scope/procedures/frames.sql +++ b/packages/app-scope/revert/schemas/app_scope/procedures/frames.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION app_scope.frames; +DROP FUNCTION app_scope.frames(uuid, text, uuid); COMMIT; diff --git a/packages/app-scope/revert/schemas/app_scope/procedures/local_frames.sql b/packages/app-scope/revert/schemas/app_scope/procedures/local_frames.sql index 7387c8c11..5dfcca786 100644 --- a/packages/app-scope/revert/schemas/app_scope/procedures/local_frames.sql +++ b/packages/app-scope/revert/schemas/app_scope/procedures/local_frames.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION app_scope.local_frames; +DROP FUNCTION app_scope.local_frames(uuid, text, uuid); COMMIT; diff --git a/packages/app-scope/revert/schemas/app_scope/procedures/membership_parent.sql b/packages/app-scope/revert/schemas/app_scope/procedures/membership_parent.sql index e0b7e9379..d95dcf412 100644 --- a/packages/app-scope/revert/schemas/app_scope/procedures/membership_parent.sql +++ b/packages/app-scope/revert/schemas/app_scope/procedures/membership_parent.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION app_scope.membership_parent; +DROP FUNCTION app_scope.membership_parent(uuid, text); COMMIT; diff --git a/packages/app-scope/revert/schemas/app_scope/procedures/platform_database_id.sql b/packages/app-scope/revert/schemas/app_scope/procedures/platform_database_id.sql index ca9d01f7d..52795b651 100644 --- a/packages/app-scope/revert/schemas/app_scope/procedures/platform_database_id.sql +++ b/packages/app-scope/revert/schemas/app_scope/procedures/platform_database_id.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION app_scope.platform_database_id; +DROP FUNCTION app_scope.platform_database_id(); COMMIT; diff --git a/packages/app-scope/revert/schemas/app_scope/procedures/projected_parent.sql b/packages/app-scope/revert/schemas/app_scope/procedures/projected_parent.sql new file mode 100644 index 000000000..decb23dc1 --- /dev/null +++ b/packages/app-scope/revert/schemas/app_scope/procedures/projected_parent.sql @@ -0,0 +1,7 @@ +-- Revert schemas/app_scope/procedures/projected_parent from pg + +BEGIN; + +DROP FUNCTION app_scope.projected_parent(uuid, text); + +COMMIT; diff --git a/packages/app-scope/revert/schemas/app_scope/procedures/routing_tables.sql b/packages/app-scope/revert/schemas/app_scope/procedures/routing_tables.sql index bf86bf74e..a01bc4d60 100644 --- a/packages/app-scope/revert/schemas/app_scope/procedures/routing_tables.sql +++ b/packages/app-scope/revert/schemas/app_scope/procedures/routing_tables.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION app_scope.routing_tables; +DROP FUNCTION app_scope.routing_tables(uuid, text); COMMIT; diff --git a/packages/app-scope/sql/pgpm-app-scope--0.36.0.bundle.tar.gz b/packages/app-scope/sql/pgpm-app-scope--0.36.0.bundle.tar.gz deleted file mode 100644 index 0c1234a97..000000000 Binary files a/packages/app-scope/sql/pgpm-app-scope--0.36.0.bundle.tar.gz and /dev/null differ diff --git a/packages/app-scope/sql/pgpm-app-scope--0.39.0.bundle.tar.gz b/packages/app-scope/sql/pgpm-app-scope--0.39.0.bundle.tar.gz new file mode 100644 index 000000000..00581377b Binary files /dev/null and b/packages/app-scope/sql/pgpm-app-scope--0.39.0.bundle.tar.gz differ diff --git a/packages/app-scope/sql/pgpm-app-scope--0.36.0.sql b/packages/app-scope/sql/pgpm-app-scope--0.39.0.sql similarity index 85% rename from packages/app-scope/sql/pgpm-app-scope--0.36.0.sql rename to packages/app-scope/sql/pgpm-app-scope--0.39.0.sql index af455a3a1..d7ccc7d18 100644 --- a/packages/app-scope/sql/pgpm-app-scope--0.36.0.sql +++ b/packages/app-scope/sql/pgpm-app-scope--0.39.0.sql @@ -45,6 +45,32 @@ BEGIN END; $EOFCODE$ LANGUAGE plpgsql STABLE SECURITY DEFINER; +CREATE FUNCTION app_scope.projected_parent( + database_id uuid, + scope text +) RETURNS TABLE ( + membership_type int, + parent_scope text +) AS $EOFCODE$ +BEGIN + IF to_regclass('scope_private.scope_types') IS NULL THEN + RETURN; + END IF; + + RETURN QUERY + SELECT st.membership_type, + parent.scope + FROM scope_private.scope_types st + LEFT JOIN scope_private.scope_types parent + ON parent.database_id = st.database_id + AND parent.membership_type = st.parent_membership_type + WHERE st.database_id = projected_parent.database_id + AND st.scope = projected_parent.scope; +END; +$EOFCODE$ LANGUAGE plpgsql STABLE SECURITY DEFINER; + +COMMENT ON FUNCTION app_scope.projected_parent(uuid, text) IS 'One scope''s membership type and parent scope, read from scope_private.scope_types with static SQL. Returns no row when the projection is absent or does not cover the scope, so callers fall back to the dynamic probe.'; + CREATE FUNCTION app_scope.membership_parent( database_id uuid, scope text @@ -66,45 +92,56 @@ DECLARE v_entity_table_owner_id uuid; v_query text; BEGIN - SELECT mtm.table_id - INTO v_types_table_id - FROM metaschema_modules_public.membership_types_module mtm - WHERE mtm.database_id = membership_parent.database_id; - - IF v_types_table_id IS NULL THEN - RETURN; - END IF; + -- Projection first: both hops in one static query, and no metadata lookups to + -- locate a hash-named table. + SELECT pp.membership_type, pp.parent_scope + INTO v_membership_type, v_parent_scope + FROM app_scope.projected_parent( + membership_parent.database_id, + membership_parent.scope + ) pp; - -- Locate the physical membership_types table (inline schema_and_table). - SELECT s.schema_name, t.name - INTO v_types_schema, v_types_table - FROM metaschema_public.schema s - JOIN metaschema_public."table" t ON (t.schema_id = s.id AND t.database_id = s.database_id) - WHERE t.id = v_types_table_id; + IF v_membership_type IS NULL THEN + SELECT mtm.table_id + INTO v_types_table_id + FROM metaschema_modules_public.membership_types_module mtm + WHERE mtm.database_id = membership_parent.database_id; - IF NOT FOUND THEN - RETURN; - END IF; + IF v_types_table_id IS NULL THEN + RETURN; + END IF; - -- SELECT id, parent_membership_type FROM "" WHERE scope = $1 - v_query := format( - 'SELECT id, parent_membership_type FROM %I.%I WHERE scope = $1', - v_types_schema, v_types_table - ); - EXECUTE v_query INTO v_membership_type, v_parent_membership_type USING membership_parent.scope; + -- Locate the physical membership_types table (inline schema_and_table). + SELECT s.schema_name, t.name + INTO v_types_schema, v_types_table + FROM metaschema_public.schema s + JOIN metaschema_public."table" t ON (t.schema_id = s.id AND t.database_id = s.database_id) + WHERE t.id = v_types_table_id; - IF v_membership_type IS NULL THEN - RETURN; - END IF; + IF NOT FOUND THEN + RETURN; + END IF; - -- Resolve the parent scope name (custom/org/app parents alike). - IF v_parent_membership_type IS NOT NULL THEN - -- SELECT scope FROM "" WHERE id = $1 + -- SELECT id, parent_membership_type FROM "" WHERE scope = $1 v_query := format( - 'SELECT scope FROM %I.%I WHERE id = $1', + 'SELECT id, parent_membership_type FROM %I.%I WHERE scope = $1', v_types_schema, v_types_table ); - EXECUTE v_query INTO v_parent_scope USING v_parent_membership_type; + EXECUTE v_query INTO v_membership_type, v_parent_membership_type USING membership_parent.scope; + + IF v_membership_type IS NULL THEN + RETURN; + END IF; + + -- Resolve the parent scope name (custom/org/app parents alike). + IF v_parent_membership_type IS NOT NULL THEN + -- SELECT scope FROM "" WHERE id = $1 + v_query := format( + 'SELECT scope FROM %I.%I WHERE id = $1', + v_types_schema, v_types_table + ); + EXECUTE v_query INTO v_parent_scope USING v_parent_membership_type; + END IF; END IF; -- Entity table + owner FK for the current scope (static metaschema config). @@ -323,6 +360,22 @@ BEGIN RETURN; END IF; + -- Before falling through to the platform's shared planes, a tenant execution + -- resolves the execution database's OWN `database` frame: a surface the + -- tenant has provisioned for itself must answer ahead of the shared platform + -- plane the fall-through would otherwise serve for that same tenant key. The + -- fall-through already re-keys the platform `database` frame by this tenant + -- (below), so both frames name the identity (database, this tenant); the + -- tenant's own is strictly more specific and comes first. Skipped for a + -- `database` execution (its local chain already emitted this frame) and for + -- the platform database itself (which has no tenant fall-through). + IF NOT v_is_platform_db AND frames.execution_scope <> 'database' THEN + scope := 'database'; + lookup_database_id := frames.database_id; + key_value := frames.database_id; + RETURN NEXT; + END IF; + -- Fall through to the platform database's OWN full local chain (its -- database -> org -> app), unless the execution already ran inside the -- platform database (in which case its local chain above already covered it). diff --git a/packages/app-scope/verify/schemas/app_scope/procedures/dyn_lookup_uuid.sql b/packages/app-scope/verify/schemas/app_scope/procedures/dyn_lookup_uuid.sql index a4d287fec..ad90d9c9d 100644 --- a/packages/app-scope/verify/schemas/app_scope/procedures/dyn_lookup_uuid.sql +++ b/packages/app-scope/verify/schemas/app_scope/procedures/dyn_lookup_uuid.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('app_scope.dyn_lookup_uuid'); +SELECT assert_function('app_scope.dyn_lookup_uuid(text, text, text, uuid)'::regprocedure); ROLLBACK; diff --git a/packages/app-scope/verify/schemas/app_scope/procedures/frames.sql b/packages/app-scope/verify/schemas/app_scope/procedures/frames.sql index 34d79cfc2..a773adac5 100644 --- a/packages/app-scope/verify/schemas/app_scope/procedures/frames.sql +++ b/packages/app-scope/verify/schemas/app_scope/procedures/frames.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('app_scope.frames'); +SELECT assert_function('app_scope.frames(uuid, text, uuid)'::regprocedure); ROLLBACK; diff --git a/packages/app-scope/verify/schemas/app_scope/procedures/local_frames.sql b/packages/app-scope/verify/schemas/app_scope/procedures/local_frames.sql index b95ab961b..6ecae630f 100644 --- a/packages/app-scope/verify/schemas/app_scope/procedures/local_frames.sql +++ b/packages/app-scope/verify/schemas/app_scope/procedures/local_frames.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('app_scope.local_frames'); +SELECT assert_function('app_scope.local_frames(uuid, text, uuid)'::regprocedure); ROLLBACK; diff --git a/packages/app-scope/verify/schemas/app_scope/procedures/membership_parent.sql b/packages/app-scope/verify/schemas/app_scope/procedures/membership_parent.sql index 092067279..e6b9add19 100644 --- a/packages/app-scope/verify/schemas/app_scope/procedures/membership_parent.sql +++ b/packages/app-scope/verify/schemas/app_scope/procedures/membership_parent.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('app_scope.membership_parent'); +SELECT assert_function('app_scope.membership_parent(uuid, text)'::regprocedure); ROLLBACK; diff --git a/packages/app-scope/verify/schemas/app_scope/procedures/platform_database_id.sql b/packages/app-scope/verify/schemas/app_scope/procedures/platform_database_id.sql index 003d3a44e..262f1e8a6 100644 --- a/packages/app-scope/verify/schemas/app_scope/procedures/platform_database_id.sql +++ b/packages/app-scope/verify/schemas/app_scope/procedures/platform_database_id.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('app_scope.platform_database_id'); +SELECT assert_function('app_scope.platform_database_id()'::regprocedure); ROLLBACK; diff --git a/packages/app-scope/verify/schemas/app_scope/procedures/projected_parent.sql b/packages/app-scope/verify/schemas/app_scope/procedures/projected_parent.sql new file mode 100644 index 000000000..189a54f2a --- /dev/null +++ b/packages/app-scope/verify/schemas/app_scope/procedures/projected_parent.sql @@ -0,0 +1,7 @@ +-- Verify schemas/app_scope/procedures/projected_parent on pg + +BEGIN; + +SELECT assert_function('app_scope.projected_parent(uuid, text)'::regprocedure); + +ROLLBACK; diff --git a/packages/app-scope/verify/schemas/app_scope/procedures/routing_tables.sql b/packages/app-scope/verify/schemas/app_scope/procedures/routing_tables.sql index d3b334e07..02047ecd6 100644 --- a/packages/app-scope/verify/schemas/app_scope/procedures/routing_tables.sql +++ b/packages/app-scope/verify/schemas/app_scope/procedures/routing_tables.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('app_scope.routing_tables'); +SELECT assert_function('app_scope.routing_tables(uuid, text)'::regprocedure); ROLLBACK; diff --git a/packages/app-scope/verify/schemas/app_scope/schema.sql b/packages/app-scope/verify/schemas/app_scope/schema.sql index 1131a7d44..b79b1ca39 100644 --- a/packages/app-scope/verify/schemas/app_scope/schema.sql +++ b/packages/app-scope/verify/schemas/app_scope/schema.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_schema ('app_scope'); +SELECT assert_schema('app_scope'::regnamespace); ROLLBACK; diff --git a/packages/base32/Makefile b/packages/base32/Makefile index 1fb579fbd..fe0eb0a53 100644 --- a/packages/base32/Makefile +++ b/packages/base32/Makefile @@ -1,5 +1,5 @@ EXTENSION = pgpm-base32 -DATA = sql/pgpm-base32--0.36.0.sql +DATA = sql/pgpm-base32--0.39.0.sql PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) diff --git a/packages/base32/__tests__/base32.decode.test.ts b/packages/base32/__tests__/base32.decode.test.ts index 879b6c0f4..3c0b3e9e4 100644 --- a/packages/base32/__tests__/base32.decode.test.ts +++ b/packages/base32/__tests__/base32.decode.test.ts @@ -93,3 +93,10 @@ cases( { result: 'foobar', name: 'MZXW6YTBOI======' } ] ); + +it('base32.decode rejects keys whose bytes contain a NUL', async () => { + // text cannot hold a NUL byte; totp.base32_to_hex exists to avoid this path + await expect( + pg.one(`SELECT base32.decode($1::text) AS decode`, ['AAAAAAAA']) + ).rejects.toThrow(/null character not permitted/); +}); diff --git a/packages/base32/package.json b/packages/base32/package.json index 5cf19a0c2..98be14d9f 100644 --- a/packages/base32/package.json +++ b/packages/base32/package.json @@ -1,6 +1,6 @@ { "name": "@pgpm/base32", - "version": "0.36.0", + "version": "0.39.0", "description": "Base32 encoding and decoding functions for PostgreSQL", "author": "Dan Lynch ", "contributors": [ diff --git a/packages/base32/pgpm-base32.control b/packages/base32/pgpm-base32.control index 1c18a70fa..b980cbbc1 100644 --- a/packages/base32/pgpm-base32.control +++ b/packages/base32/pgpm-base32.control @@ -1,6 +1,6 @@ # pgpm-base32 extension comment = 'pgpm-base32 extension' -default_version = '0.36.0' +default_version = '0.39.0' module_pathname = '$libdir/pgpm-base32' requires = 'pgcrypto,plpgsql,pgpm-verify' relocatable = false diff --git a/packages/base32/revert/schemas/base32/procedures/decode.sql b/packages/base32/revert/schemas/base32/procedures/decode.sql index 11da51b18..a4d33e572 100644 --- a/packages/base32/revert/schemas/base32/procedures/decode.sql +++ b/packages/base32/revert/schemas/base32/procedures/decode.sql @@ -2,12 +2,12 @@ BEGIN; -DROP FUNCTION base32.decode; -DROP FUNCTION base32.valid; -DROP FUNCTION base32.zero_fill; -DROP FUNCTION base32.base32_alphabet_to_decimal_int; -DROP FUNCTION base32.decimal_to_chunks; -DROP FUNCTION base32.base32_to_decimal; -DROP FUNCTION base32.base32_alphabet_to_decimal; +DROP FUNCTION base32.decode(text); +DROP FUNCTION base32.valid(text); +DROP FUNCTION base32.zero_fill(int4, int4); +DROP FUNCTION base32.base32_alphabet_to_decimal_int(text); +DROP FUNCTION base32.decimal_to_chunks(text[]); +DROP FUNCTION base32.base32_to_decimal(text); +DROP FUNCTION base32.base32_alphabet_to_decimal(text); COMMIT; diff --git a/packages/base32/revert/schemas/base32/procedures/encode.sql b/packages/base32/revert/schemas/base32/procedures/encode.sql index d029387bf..f1863ada7 100644 --- a/packages/base32/revert/schemas/base32/procedures/encode.sql +++ b/packages/base32/revert/schemas/base32/procedures/encode.sql @@ -2,17 +2,17 @@ BEGIN; -DROP FUNCTION base32.encode; -DROP FUNCTION base32.to_base32; -DROP FUNCTION base32.base32_alphabet; -DROP FUNCTION base32.to_decimal; -DROP FUNCTION base32.fill_chunks; -DROP FUNCTION base32.to_chunks; -DROP FUNCTION base32.string_nchars; -DROP FUNCTION base32.to_groups; -DROP FUNCTION base32.to_binary(int[]); -DROP FUNCTION base32.to_binary(int); -DROP FUNCTION base32.to_ascii; -DROP FUNCTION base32.binary_to_int; +DROP FUNCTION base32.encode(text); +DROP FUNCTION base32.to_base32(text[]); +DROP FUNCTION base32.base32_alphabet(int4); +DROP FUNCTION base32.to_decimal(text[]); +DROP FUNCTION base32.fill_chunks(text[]); +DROP FUNCTION base32.to_chunks(text[]); +DROP FUNCTION base32.string_nchars(text, int4); +DROP FUNCTION base32.to_groups(text[]); +DROP FUNCTION base32.to_binary(int4[]); +DROP FUNCTION base32.to_binary(int4); +DROP FUNCTION base32.to_ascii(text); +DROP FUNCTION base32.binary_to_int(text); COMMIT; diff --git a/packages/base32/sql/pgpm-base32--0.36.0.bundle.tar.gz b/packages/base32/sql/pgpm-base32--0.36.0.bundle.tar.gz deleted file mode 100644 index 4025aeaf3..000000000 Binary files a/packages/base32/sql/pgpm-base32--0.36.0.bundle.tar.gz and /dev/null differ diff --git a/packages/base32/sql/pgpm-base32--0.39.0.bundle.tar.gz b/packages/base32/sql/pgpm-base32--0.39.0.bundle.tar.gz new file mode 100644 index 000000000..8d5107062 Binary files /dev/null and b/packages/base32/sql/pgpm-base32--0.39.0.bundle.tar.gz differ diff --git a/packages/base32/sql/pgpm-base32--0.36.0.sql b/packages/base32/sql/pgpm-base32--0.39.0.sql similarity index 100% rename from packages/base32/sql/pgpm-base32--0.36.0.sql rename to packages/base32/sql/pgpm-base32--0.39.0.sql diff --git a/packages/base32/verify/schemas/base32/procedures/decode.sql b/packages/base32/verify/schemas/base32/procedures/decode.sql index fb03d980a..681184a43 100644 --- a/packages/base32/verify/schemas/base32/procedures/decode.sql +++ b/packages/base32/verify/schemas/base32/procedures/decode.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('base32.decode'); +SELECT assert_function('base32.decode(text)'::regprocedure); ROLLBACK; diff --git a/packages/base32/verify/schemas/base32/procedures/encode.sql b/packages/base32/verify/schemas/base32/procedures/encode.sql index 53de63f7a..10374ccec 100644 --- a/packages/base32/verify/schemas/base32/procedures/encode.sql +++ b/packages/base32/verify/schemas/base32/procedures/encode.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('base32.encode'); +SELECT assert_function('base32.encode(text)'::regprocedure); ROLLBACK; diff --git a/packages/base32/verify/schemas/base32/schema.sql b/packages/base32/verify/schemas/base32/schema.sql index 5cc71cd5d..42cf5e8c4 100644 --- a/packages/base32/verify/schemas/base32/schema.sql +++ b/packages/base32/verify/schemas/base32/schema.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_schema ('base32'); +SELECT assert_schema('base32'::regnamespace); ROLLBACK; diff --git a/packages/database-jobs/Makefile b/packages/database-jobs/Makefile index 4621a156d..fa1c3e7f3 100644 --- a/packages/database-jobs/Makefile +++ b/packages/database-jobs/Makefile @@ -1,5 +1,5 @@ EXTENSION = pgpm-database-jobs -DATA = sql/pgpm-database-jobs--0.36.0.sql +DATA = sql/pgpm-database-jobs--0.39.0.sql PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) diff --git a/packages/database-jobs/package.json b/packages/database-jobs/package.json index cb6162438..2cdde3c5f 100644 --- a/packages/database-jobs/package.json +++ b/packages/database-jobs/package.json @@ -1,6 +1,6 @@ { "name": "@pgpm/database-jobs", - "version": "0.36.0", + "version": "0.39.0", "description": "Database-specific job handling and queue management", "author": "Dan Lynch ", "contributors": [ diff --git a/packages/database-jobs/pgpm-database-jobs.control b/packages/database-jobs/pgpm-database-jobs.control index 965b36dd0..777101a8a 100644 --- a/packages/database-jobs/pgpm-database-jobs.control +++ b/packages/database-jobs/pgpm-database-jobs.control @@ -1,6 +1,6 @@ # pgpm-database-jobs extension comment = 'pgpm-database-jobs extension' -default_version = '0.36.0' +default_version = '0.39.0' module_pathname = '$libdir/pgpm-database-jobs' requires = 'plpgsql,pgcrypto,pgpm-verify,pgpm-jwt-claims' relocatable = false diff --git a/packages/database-jobs/pgpm.plan b/packages/database-jobs/pgpm.plan index d70d89d74..7e4b6e919 100644 --- a/packages/database-jobs/pgpm.plan +++ b/packages/database-jobs/pgpm.plan @@ -1,7 +1,7 @@ %syntax-version=1.0.0 %project=pgpm-database-jobs %uri=pgpm-database-jobs -schemas/app_jobs/schema [pgpm-verify:@0.1.0] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/schema +schemas/app_jobs/schema [pgpm-verify:procedures/assert_function] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/schema schemas/app_jobs/triggers/tg_update_timestamps [schemas/app_jobs/schema] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/triggers/tg_update_timestamps schemas/app_jobs/triggers/tg_add_job_with_row_id [schemas/app_jobs/schema] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/triggers/tg_add_job_with_row_id schemas/app_jobs/triggers/tg_add_job_with_row [schemas/app_jobs/schema] 2025-08-26T23:57:41Z pgpm # add schemas/app_jobs/triggers/tg_add_job_with_row diff --git a/packages/database-jobs/revert/schemas/app_jobs/helpers/json_build_object_apply.sql b/packages/database-jobs/revert/schemas/app_jobs/helpers/json_build_object_apply.sql index b1778898c..9771f00af 100644 --- a/packages/database-jobs/revert/schemas/app_jobs/helpers/json_build_object_apply.sql +++ b/packages/database-jobs/revert/schemas/app_jobs/helpers/json_build_object_apply.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION app_jobs.json_build_object_apply; +DROP FUNCTION app_jobs.json_build_object_apply(text[]); COMMIT; diff --git a/packages/database-jobs/revert/schemas/app_jobs/procedures/add_job.sql b/packages/database-jobs/revert/schemas/app_jobs/procedures/add_job.sql index 44a65ae8d..10b801c1b 100644 --- a/packages/database-jobs/revert/schemas/app_jobs/procedures/add_job.sql +++ b/packages/database-jobs/revert/schemas/app_jobs/procedures/add_job.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION app_jobs.add_job; +DROP FUNCTION app_jobs.add_job(text, json, text, text, timestamptz, int4, int4, uuid, uuid, text, uuid, text, uuid); COMMIT; diff --git a/packages/database-jobs/revert/schemas/app_jobs/procedures/add_scheduled_job.sql b/packages/database-jobs/revert/schemas/app_jobs/procedures/add_scheduled_job.sql index 882a98f9d..d6f6cb544 100644 --- a/packages/database-jobs/revert/schemas/app_jobs/procedures/add_scheduled_job.sql +++ b/packages/database-jobs/revert/schemas/app_jobs/procedures/add_scheduled_job.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION app_jobs.add_scheduled_job; +DROP FUNCTION app_jobs.add_scheduled_job(text, json, json, text, text, int4, int4, uuid, uuid); COMMIT; diff --git a/packages/database-jobs/revert/schemas/app_jobs/procedures/complete_job.sql b/packages/database-jobs/revert/schemas/app_jobs/procedures/complete_job.sql index 7c0ea9dfa..c45460937 100644 --- a/packages/database-jobs/revert/schemas/app_jobs/procedures/complete_job.sql +++ b/packages/database-jobs/revert/schemas/app_jobs/procedures/complete_job.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION app_jobs.complete_job; +DROP FUNCTION app_jobs.complete_job(text, int8); COMMIT; diff --git a/packages/database-jobs/revert/schemas/app_jobs/procedures/complete_jobs.sql b/packages/database-jobs/revert/schemas/app_jobs/procedures/complete_jobs.sql index 3db9150e3..49ab22dbc 100644 --- a/packages/database-jobs/revert/schemas/app_jobs/procedures/complete_jobs.sql +++ b/packages/database-jobs/revert/schemas/app_jobs/procedures/complete_jobs.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION app_jobs.complete_jobs; +DROP FUNCTION app_jobs.complete_jobs(int8[]); COMMIT; diff --git a/packages/database-jobs/revert/schemas/app_jobs/procedures/do_notify.sql b/packages/database-jobs/revert/schemas/app_jobs/procedures/do_notify.sql index 58a8138a1..54cb06fd0 100644 --- a/packages/database-jobs/revert/schemas/app_jobs/procedures/do_notify.sql +++ b/packages/database-jobs/revert/schemas/app_jobs/procedures/do_notify.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION app_jobs.do_notify; +DROP FUNCTION app_jobs.do_notify(); COMMIT; diff --git a/packages/database-jobs/revert/schemas/app_jobs/procedures/fail_job.sql b/packages/database-jobs/revert/schemas/app_jobs/procedures/fail_job.sql index ed96e401e..b29411dcb 100644 --- a/packages/database-jobs/revert/schemas/app_jobs/procedures/fail_job.sql +++ b/packages/database-jobs/revert/schemas/app_jobs/procedures/fail_job.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION app_jobs.fail_job; +DROP FUNCTION app_jobs.fail_job(text, int8, text); COMMIT; diff --git a/packages/database-jobs/revert/schemas/app_jobs/procedures/force_unlock_workers.sql b/packages/database-jobs/revert/schemas/app_jobs/procedures/force_unlock_workers.sql index aac5d270a..91e570480 100644 --- a/packages/database-jobs/revert/schemas/app_jobs/procedures/force_unlock_workers.sql +++ b/packages/database-jobs/revert/schemas/app_jobs/procedures/force_unlock_workers.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION app_jobs.force_unlock_workers; +DROP FUNCTION app_jobs.force_unlock_workers(text[]); COMMIT; diff --git a/packages/database-jobs/revert/schemas/app_jobs/procedures/get_job.sql b/packages/database-jobs/revert/schemas/app_jobs/procedures/get_job.sql index 469f6b4da..0251a8a04 100644 --- a/packages/database-jobs/revert/schemas/app_jobs/procedures/get_job.sql +++ b/packages/database-jobs/revert/schemas/app_jobs/procedures/get_job.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION app_jobs.get_job; +DROP FUNCTION app_jobs.get_job(text, text[], interval); COMMIT; diff --git a/packages/database-jobs/revert/schemas/app_jobs/procedures/get_scheduled_job.sql b/packages/database-jobs/revert/schemas/app_jobs/procedures/get_scheduled_job.sql index f41f8fdb4..b51c7545d 100644 --- a/packages/database-jobs/revert/schemas/app_jobs/procedures/get_scheduled_job.sql +++ b/packages/database-jobs/revert/schemas/app_jobs/procedures/get_scheduled_job.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION app_jobs.get_scheduled_job; +DROP FUNCTION app_jobs.get_scheduled_job(text, text[]); COMMIT; diff --git a/packages/database-jobs/revert/schemas/app_jobs/procedures/permanently_fail_jobs.sql b/packages/database-jobs/revert/schemas/app_jobs/procedures/permanently_fail_jobs.sql index f0299ea82..c906e91c5 100644 --- a/packages/database-jobs/revert/schemas/app_jobs/procedures/permanently_fail_jobs.sql +++ b/packages/database-jobs/revert/schemas/app_jobs/procedures/permanently_fail_jobs.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION app_jobs.permanently_fail_jobs; +DROP FUNCTION app_jobs.permanently_fail_jobs(int8[], text); COMMIT; diff --git a/packages/database-jobs/revert/schemas/app_jobs/procedures/release_jobs.sql b/packages/database-jobs/revert/schemas/app_jobs/procedures/release_jobs.sql index 8ece69ef1..6b6ce713e 100644 --- a/packages/database-jobs/revert/schemas/app_jobs/procedures/release_jobs.sql +++ b/packages/database-jobs/revert/schemas/app_jobs/procedures/release_jobs.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION app_jobs.release_jobs; +DROP FUNCTION app_jobs.release_jobs(text); COMMIT; diff --git a/packages/database-jobs/revert/schemas/app_jobs/procedures/release_scheduled_jobs.sql b/packages/database-jobs/revert/schemas/app_jobs/procedures/release_scheduled_jobs.sql index a16e6e9a4..33005e6e3 100644 --- a/packages/database-jobs/revert/schemas/app_jobs/procedures/release_scheduled_jobs.sql +++ b/packages/database-jobs/revert/schemas/app_jobs/procedures/release_scheduled_jobs.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION app_jobs.release_scheduled_jobs; +DROP FUNCTION app_jobs.release_scheduled_jobs(text, int8[]); COMMIT; diff --git a/packages/database-jobs/revert/schemas/app_jobs/procedures/remove_job.sql b/packages/database-jobs/revert/schemas/app_jobs/procedures/remove_job.sql index e673bdee4..3008be766 100644 --- a/packages/database-jobs/revert/schemas/app_jobs/procedures/remove_job.sql +++ b/packages/database-jobs/revert/schemas/app_jobs/procedures/remove_job.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION app_jobs.remove_job; +DROP FUNCTION app_jobs.remove_job(text); COMMIT; diff --git a/packages/database-jobs/revert/schemas/app_jobs/procedures/reschedule_jobs.sql b/packages/database-jobs/revert/schemas/app_jobs/procedures/reschedule_jobs.sql index 34a441716..f62607d26 100644 --- a/packages/database-jobs/revert/schemas/app_jobs/procedures/reschedule_jobs.sql +++ b/packages/database-jobs/revert/schemas/app_jobs/procedures/reschedule_jobs.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION app_jobs.reschedule_jobs; +DROP FUNCTION app_jobs.reschedule_jobs(int8[], timestamptz, int4, int4, int4); COMMIT; diff --git a/packages/database-jobs/revert/schemas/app_jobs/procedures/run_scheduled_job.sql b/packages/database-jobs/revert/schemas/app_jobs/procedures/run_scheduled_job.sql index 77886fc04..f89036df0 100644 --- a/packages/database-jobs/revert/schemas/app_jobs/procedures/run_scheduled_job.sql +++ b/packages/database-jobs/revert/schemas/app_jobs/procedures/run_scheduled_job.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION app_jobs.run_scheduled_job; +DROP FUNCTION app_jobs.run_scheduled_job(int8, interval); COMMIT; diff --git a/packages/database-jobs/revert/schemas/app_jobs/procedures/schedule_min_interval_seconds.sql b/packages/database-jobs/revert/schemas/app_jobs/procedures/schedule_min_interval_seconds.sql index c9fc24694..9fcc2bf91 100644 --- a/packages/database-jobs/revert/schemas/app_jobs/procedures/schedule_min_interval_seconds.sql +++ b/packages/database-jobs/revert/schemas/app_jobs/procedures/schedule_min_interval_seconds.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION app_jobs.schedule_min_interval_seconds; +DROP FUNCTION app_jobs.schedule_min_interval_seconds(json); COMMIT; diff --git a/packages/database-jobs/revert/schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count.sql b/packages/database-jobs/revert/schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count.sql index bf4f88c6f..3181fcce4 100644 --- a/packages/database-jobs/revert/schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count.sql +++ b/packages/database-jobs/revert/schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count.sql @@ -1,7 +1,9 @@ -- Revert schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count from pg + BEGIN; -DROP TRIGGER decrease_job_queue_count_on_delete ON app_jobs.jobs; + DROP TRIGGER decrease_job_queue_count_on_update ON app_jobs.jobs; -DROP FUNCTION app_jobs.tg_decrease_job_queue_count; -COMMIT; +DROP TRIGGER decrease_job_queue_count_on_delete ON app_jobs.jobs; +DROP FUNCTION app_jobs.tg_decrease_job_queue_count(); +COMMIT; diff --git a/packages/database-jobs/revert/schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count.sql b/packages/database-jobs/revert/schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count.sql index 5098a6517..4a4094130 100644 --- a/packages/database-jobs/revert/schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count.sql +++ b/packages/database-jobs/revert/schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count.sql @@ -1,7 +1,9 @@ -- Revert schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count from pg + BEGIN; -DROP TRIGGER _500_increase_job_queue_count_on_insert ON app_jobs.jobs; + DROP TRIGGER _500_increase_job_queue_count_on_update ON app_jobs.jobs; -DROP FUNCTION app_jobs.tg_increase_job_queue_count; -COMMIT; +DROP TRIGGER _500_increase_job_queue_count_on_insert ON app_jobs.jobs; +DROP FUNCTION app_jobs.tg_increase_job_queue_count(); +COMMIT; diff --git a/packages/database-jobs/revert/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql b/packages/database-jobs/revert/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql index 37a5f531e..fdff5cc03 100644 --- a/packages/database-jobs/revert/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql +++ b/packages/database-jobs/revert/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql @@ -1,6 +1,8 @@ -- Revert schemas/app_jobs/tables/jobs/triggers/notify_worker from pg + BEGIN; -DROP TRIGGER IF EXISTS _900_notify_worker ON app_jobs.jobs; -DROP TRIGGER IF EXISTS _900_after_insert ON app_jobs.jobs; -DROP FUNCTION IF EXISTS app_jobs.tg_jobs__after_insert; + +DROP TRIGGER _900_after_insert ON app_jobs.jobs; +DROP FUNCTION app_jobs.tg_jobs__after_insert(); + COMMIT; diff --git a/packages/database-jobs/revert/schemas/app_jobs/triggers/tg_add_job_with_fields.sql b/packages/database-jobs/revert/schemas/app_jobs/triggers/tg_add_job_with_fields.sql index 5384edfc3..5ac40f23e 100644 --- a/packages/database-jobs/revert/schemas/app_jobs/triggers/tg_add_job_with_fields.sql +++ b/packages/database-jobs/revert/schemas/app_jobs/triggers/tg_add_job_with_fields.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION app_jobs.trigger_job_with_fields; +DROP FUNCTION app_jobs.trigger_job_with_fields(); COMMIT; diff --git a/packages/database-jobs/revert/schemas/app_jobs/triggers/tg_add_job_with_row.sql b/packages/database-jobs/revert/schemas/app_jobs/triggers/tg_add_job_with_row.sql index 9d6b68a2c..ad603bfbe 100644 --- a/packages/database-jobs/revert/schemas/app_jobs/triggers/tg_add_job_with_row.sql +++ b/packages/database-jobs/revert/schemas/app_jobs/triggers/tg_add_job_with_row.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION app_jobs.tg_add_job_with_row; +DROP FUNCTION app_jobs.tg_add_job_with_row(); COMMIT; diff --git a/packages/database-jobs/revert/schemas/app_jobs/triggers/tg_add_job_with_row_id.sql b/packages/database-jobs/revert/schemas/app_jobs/triggers/tg_add_job_with_row_id.sql index 1f0fb04be..9e61e9f15 100644 --- a/packages/database-jobs/revert/schemas/app_jobs/triggers/tg_add_job_with_row_id.sql +++ b/packages/database-jobs/revert/schemas/app_jobs/triggers/tg_add_job_with_row_id.sql @@ -1,5 +1,5 @@ -- Revert schemas/app_jobs/triggers/tg_add_job_with_row_id from pg BEGIN; -DROP FUNCTION app_jobs.tg_add_job_with_row_id; +DROP FUNCTION app_jobs.tg_add_job_with_row_id(); COMMIT; diff --git a/packages/database-jobs/revert/schemas/app_jobs/triggers/tg_update_timestamps.sql b/packages/database-jobs/revert/schemas/app_jobs/triggers/tg_update_timestamps.sql index 37378b148..e08b9a570 100644 --- a/packages/database-jobs/revert/schemas/app_jobs/triggers/tg_update_timestamps.sql +++ b/packages/database-jobs/revert/schemas/app_jobs/triggers/tg_update_timestamps.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION app_jobs.tg_update_timestamps; +DROP FUNCTION app_jobs.tg_update_timestamps(); COMMIT; diff --git a/packages/database-jobs/sql/pgpm-database-jobs--0.36.0.bundle.tar.gz b/packages/database-jobs/sql/pgpm-database-jobs--0.36.0.bundle.tar.gz deleted file mode 100644 index 6649f5fd9..000000000 Binary files a/packages/database-jobs/sql/pgpm-database-jobs--0.36.0.bundle.tar.gz and /dev/null differ diff --git a/packages/database-jobs/sql/pgpm-database-jobs--0.39.0.bundle.tar.gz b/packages/database-jobs/sql/pgpm-database-jobs--0.39.0.bundle.tar.gz new file mode 100644 index 000000000..2acbd2423 Binary files /dev/null and b/packages/database-jobs/sql/pgpm-database-jobs--0.39.0.bundle.tar.gz differ diff --git a/packages/database-jobs/sql/pgpm-database-jobs--0.36.0.sql b/packages/database-jobs/sql/pgpm-database-jobs--0.39.0.sql similarity index 100% rename from packages/database-jobs/sql/pgpm-database-jobs--0.36.0.sql rename to packages/database-jobs/sql/pgpm-database-jobs--0.39.0.sql diff --git a/packages/database-jobs/verify/schemas/app_jobs/helpers/json_build_object_apply.sql b/packages/database-jobs/verify/schemas/app_jobs/helpers/json_build_object_apply.sql index e05072cfb..d1b17fbb3 100644 --- a/packages/database-jobs/verify/schemas/app_jobs/helpers/json_build_object_apply.sql +++ b/packages/database-jobs/verify/schemas/app_jobs/helpers/json_build_object_apply.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('app_jobs.json_build_object_apply'); +SELECT assert_function('app_jobs.json_build_object_apply(text[])'::regprocedure); ROLLBACK; diff --git a/packages/database-jobs/verify/schemas/app_jobs/procedures/add_job.sql b/packages/database-jobs/verify/schemas/app_jobs/procedures/add_job.sql index c841e7d04..0c68def06 100644 --- a/packages/database-jobs/verify/schemas/app_jobs/procedures/add_job.sql +++ b/packages/database-jobs/verify/schemas/app_jobs/procedures/add_job.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('app_jobs.add_job'); +SELECT assert_function('app_jobs.add_job(text, json, text, text, timestamptz, int4, int4, uuid, uuid, text, uuid, text, uuid)'::regprocedure); ROLLBACK; diff --git a/packages/database-jobs/verify/schemas/app_jobs/procedures/add_scheduled_job.sql b/packages/database-jobs/verify/schemas/app_jobs/procedures/add_scheduled_job.sql index a2f7d4815..2c12d35bb 100644 --- a/packages/database-jobs/verify/schemas/app_jobs/procedures/add_scheduled_job.sql +++ b/packages/database-jobs/verify/schemas/app_jobs/procedures/add_scheduled_job.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('app_jobs.add_scheduled_job'); +SELECT assert_function('app_jobs.add_scheduled_job(text, json, json, text, text, int4, int4, uuid, uuid)'::regprocedure); ROLLBACK; diff --git a/packages/database-jobs/verify/schemas/app_jobs/procedures/complete_job.sql b/packages/database-jobs/verify/schemas/app_jobs/procedures/complete_job.sql index 4bd179aee..257e4f0f2 100644 --- a/packages/database-jobs/verify/schemas/app_jobs/procedures/complete_job.sql +++ b/packages/database-jobs/verify/schemas/app_jobs/procedures/complete_job.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('app_jobs.complete_job'); +SELECT assert_function('app_jobs.complete_job(text, int8)'::regprocedure); ROLLBACK; diff --git a/packages/database-jobs/verify/schemas/app_jobs/procedures/complete_jobs.sql b/packages/database-jobs/verify/schemas/app_jobs/procedures/complete_jobs.sql index aa9a5a457..53cc86bb8 100644 --- a/packages/database-jobs/verify/schemas/app_jobs/procedures/complete_jobs.sql +++ b/packages/database-jobs/verify/schemas/app_jobs/procedures/complete_jobs.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('app_jobs.complete_jobs'); +SELECT assert_function('app_jobs.complete_jobs(int8[])'::regprocedure); ROLLBACK; diff --git a/packages/database-jobs/verify/schemas/app_jobs/procedures/do_notify.sql b/packages/database-jobs/verify/schemas/app_jobs/procedures/do_notify.sql index df64a9f48..6dd5a9b7f 100644 --- a/packages/database-jobs/verify/schemas/app_jobs/procedures/do_notify.sql +++ b/packages/database-jobs/verify/schemas/app_jobs/procedures/do_notify.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('app_jobs.do_notify'); +SELECT assert_function('app_jobs.do_notify()'::regprocedure); ROLLBACK; diff --git a/packages/database-jobs/verify/schemas/app_jobs/procedures/fail_job.sql b/packages/database-jobs/verify/schemas/app_jobs/procedures/fail_job.sql index b9c65b489..d534a7336 100644 --- a/packages/database-jobs/verify/schemas/app_jobs/procedures/fail_job.sql +++ b/packages/database-jobs/verify/schemas/app_jobs/procedures/fail_job.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('app_jobs.fail_job'); +SELECT assert_function('app_jobs.fail_job(text, int8, text)'::regprocedure); ROLLBACK; diff --git a/packages/database-jobs/verify/schemas/app_jobs/procedures/force_unlock_workers.sql b/packages/database-jobs/verify/schemas/app_jobs/procedures/force_unlock_workers.sql index a71b0bbd6..6679cbeac 100644 --- a/packages/database-jobs/verify/schemas/app_jobs/procedures/force_unlock_workers.sql +++ b/packages/database-jobs/verify/schemas/app_jobs/procedures/force_unlock_workers.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('app_jobs.force_unlock_workers'); +SELECT assert_function('app_jobs.force_unlock_workers(text[])'::regprocedure); ROLLBACK; diff --git a/packages/database-jobs/verify/schemas/app_jobs/procedures/get_job.sql b/packages/database-jobs/verify/schemas/app_jobs/procedures/get_job.sql index 86170be11..5d6f06a7a 100644 --- a/packages/database-jobs/verify/schemas/app_jobs/procedures/get_job.sql +++ b/packages/database-jobs/verify/schemas/app_jobs/procedures/get_job.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('app_jobs.get_job'); +SELECT assert_function('app_jobs.get_job(text, text[], interval)'::regprocedure); ROLLBACK; diff --git a/packages/database-jobs/verify/schemas/app_jobs/procedures/get_scheduled_job.sql b/packages/database-jobs/verify/schemas/app_jobs/procedures/get_scheduled_job.sql index bb7e58d7f..6a80e0f88 100644 --- a/packages/database-jobs/verify/schemas/app_jobs/procedures/get_scheduled_job.sql +++ b/packages/database-jobs/verify/schemas/app_jobs/procedures/get_scheduled_job.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('app_jobs.get_scheduled_job'); +SELECT assert_function('app_jobs.get_scheduled_job(text, text[])'::regprocedure); ROLLBACK; diff --git a/packages/database-jobs/verify/schemas/app_jobs/procedures/grants/grant_execute_add_job_to_authenticated.sql b/packages/database-jobs/verify/schemas/app_jobs/procedures/grants/grant_execute_add_job_to_authenticated.sql index cfe2781ed..ebdd77854 100644 --- a/packages/database-jobs/verify/schemas/app_jobs/procedures/grants/grant_execute_add_job_to_authenticated.sql +++ b/packages/database-jobs/verify/schemas/app_jobs/procedures/grants/grant_execute_add_job_to_authenticated.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT has_function_privilege('authenticated', 'app_jobs.add_job(text, json, text, text, timestamptz, integer, integer, uuid, uuid, text, uuid, text, uuid)', 'EXECUTE'); +SELECT assert_function_grant('app_jobs.add_job(text, json, text, text, timestamptz, integer, integer, uuid, uuid, text, uuid, text, uuid)'::regprocedure, 'authenticated', 'EXECUTE'); ROLLBACK; diff --git a/packages/database-jobs/verify/schemas/app_jobs/procedures/grants/grant_execute_add_scheduled_job_to_authenticated.sql b/packages/database-jobs/verify/schemas/app_jobs/procedures/grants/grant_execute_add_scheduled_job_to_authenticated.sql index 451d17f40..f580b9417 100644 --- a/packages/database-jobs/verify/schemas/app_jobs/procedures/grants/grant_execute_add_scheduled_job_to_authenticated.sql +++ b/packages/database-jobs/verify/schemas/app_jobs/procedures/grants/grant_execute_add_scheduled_job_to_authenticated.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT has_function_privilege('authenticated', 'app_jobs.add_scheduled_job(text, json, json, text, text, integer, integer, uuid, uuid)', 'EXECUTE'); +SELECT assert_function_grant('app_jobs.add_scheduled_job(text, json, json, text, text, integer, integer, uuid, uuid)'::regprocedure, 'authenticated', 'EXECUTE'); ROLLBACK; diff --git a/packages/database-jobs/verify/schemas/app_jobs/procedures/permanently_fail_jobs.sql b/packages/database-jobs/verify/schemas/app_jobs/procedures/permanently_fail_jobs.sql index dfd8852f0..62596780d 100644 --- a/packages/database-jobs/verify/schemas/app_jobs/procedures/permanently_fail_jobs.sql +++ b/packages/database-jobs/verify/schemas/app_jobs/procedures/permanently_fail_jobs.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('app_jobs.permanently_fail_jobs'); +SELECT assert_function('app_jobs.permanently_fail_jobs(int8[], text)'::regprocedure); ROLLBACK; diff --git a/packages/database-jobs/verify/schemas/app_jobs/procedures/release_jobs.sql b/packages/database-jobs/verify/schemas/app_jobs/procedures/release_jobs.sql index 70004e7e1..9ff6962de 100644 --- a/packages/database-jobs/verify/schemas/app_jobs/procedures/release_jobs.sql +++ b/packages/database-jobs/verify/schemas/app_jobs/procedures/release_jobs.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('app_jobs.release_jobs'); +SELECT assert_function('app_jobs.release_jobs(text)'::regprocedure); ROLLBACK; diff --git a/packages/database-jobs/verify/schemas/app_jobs/procedures/release_scheduled_jobs.sql b/packages/database-jobs/verify/schemas/app_jobs/procedures/release_scheduled_jobs.sql index 5b9b5929a..82d6b9a64 100644 --- a/packages/database-jobs/verify/schemas/app_jobs/procedures/release_scheduled_jobs.sql +++ b/packages/database-jobs/verify/schemas/app_jobs/procedures/release_scheduled_jobs.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('app_jobs.release_scheduled_jobs'); +SELECT assert_function('app_jobs.release_scheduled_jobs(text, int8[])'::regprocedure); ROLLBACK; diff --git a/packages/database-jobs/verify/schemas/app_jobs/procedures/remove_job.sql b/packages/database-jobs/verify/schemas/app_jobs/procedures/remove_job.sql index b855f4090..7a96ef2fd 100644 --- a/packages/database-jobs/verify/schemas/app_jobs/procedures/remove_job.sql +++ b/packages/database-jobs/verify/schemas/app_jobs/procedures/remove_job.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('app_jobs.remove_job'); +SELECT assert_function('app_jobs.remove_job(text)'::regprocedure); ROLLBACK; diff --git a/packages/database-jobs/verify/schemas/app_jobs/procedures/reschedule_jobs.sql b/packages/database-jobs/verify/schemas/app_jobs/procedures/reschedule_jobs.sql index 80ab587b3..104cd33bd 100644 --- a/packages/database-jobs/verify/schemas/app_jobs/procedures/reschedule_jobs.sql +++ b/packages/database-jobs/verify/schemas/app_jobs/procedures/reschedule_jobs.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('app_jobs.reschedule_jobs'); +SELECT assert_function('app_jobs.reschedule_jobs(int8[], timestamptz, int4, int4, int4)'::regprocedure); ROLLBACK; diff --git a/packages/database-jobs/verify/schemas/app_jobs/procedures/run_scheduled_job.sql b/packages/database-jobs/verify/schemas/app_jobs/procedures/run_scheduled_job.sql index 02257023b..9f7d02cc9 100644 --- a/packages/database-jobs/verify/schemas/app_jobs/procedures/run_scheduled_job.sql +++ b/packages/database-jobs/verify/schemas/app_jobs/procedures/run_scheduled_job.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('app_jobs.run_scheduled_job'); +SELECT assert_function('app_jobs.run_scheduled_job(int8, interval)'::regprocedure); ROLLBACK; diff --git a/packages/database-jobs/verify/schemas/app_jobs/procedures/schedule_min_interval_seconds.sql b/packages/database-jobs/verify/schemas/app_jobs/procedures/schedule_min_interval_seconds.sql index 0d7c0810b..9e7d3b046 100644 --- a/packages/database-jobs/verify/schemas/app_jobs/procedures/schedule_min_interval_seconds.sql +++ b/packages/database-jobs/verify/schemas/app_jobs/procedures/schedule_min_interval_seconds.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('app_jobs.schedule_min_interval_seconds'); +SELECT assert_function('app_jobs.schedule_min_interval_seconds(json)'::regprocedure); ROLLBACK; diff --git a/packages/database-jobs/verify/schemas/app_jobs/schema.sql b/packages/database-jobs/verify/schemas/app_jobs/schema.sql index 5e0b19d49..cb33f345b 100644 --- a/packages/database-jobs/verify/schemas/app_jobs/schema.sql +++ b/packages/database-jobs/verify/schemas/app_jobs/schema.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_schema ('app_jobs'); +SELECT assert_schema('app_jobs'::regnamespace); ROLLBACK; diff --git a/packages/database-jobs/verify/schemas/app_jobs/tables/job_queues/grants/grant_select_insert_update_delete_to_administrator.sql b/packages/database-jobs/verify/schemas/app_jobs/tables/job_queues/grants/grant_select_insert_update_delete_to_administrator.sql index d645d8558..433920c99 100644 --- a/packages/database-jobs/verify/schemas/app_jobs/tables/job_queues/grants/grant_select_insert_update_delete_to_administrator.sql +++ b/packages/database-jobs/verify/schemas/app_jobs/tables/job_queues/grants/grant_select_insert_update_delete_to_administrator.sql @@ -2,9 +2,9 @@ BEGIN; - SELECT has_table_privilege('administrator', 'app_jobs.job_queues', 'SELECT'); - SELECT has_table_privilege('administrator', 'app_jobs.job_queues', 'INSERT'); - SELECT has_table_privilege('administrator', 'app_jobs.job_queues', 'UPDATE'); - SELECT has_table_privilege('administrator', 'app_jobs.job_queues', 'DELETE'); + SELECT assert_table_grant('app_jobs.job_queues'::regclass, 'administrator', 'SELECT'); + SELECT assert_table_grant('app_jobs.job_queues'::regclass, 'administrator', 'INSERT'); + SELECT assert_table_grant('app_jobs.job_queues'::regclass, 'administrator', 'UPDATE'); + SELECT assert_table_grant('app_jobs.job_queues'::regclass, 'administrator', 'DELETE'); ROLLBACK; diff --git a/packages/database-jobs/verify/schemas/app_jobs/tables/job_queues/indexes/job_queues_locked_by_idx.sql b/packages/database-jobs/verify/schemas/app_jobs/tables/job_queues/indexes/job_queues_locked_by_idx.sql index bb3786608..e5dc9536b 100644 --- a/packages/database-jobs/verify/schemas/app_jobs/tables/job_queues/indexes/job_queues_locked_by_idx.sql +++ b/packages/database-jobs/verify/schemas/app_jobs/tables/job_queues/indexes/job_queues_locked_by_idx.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_index ('app_jobs.job_queues', 'job_queues_locked_by_idx'); +SELECT assert_index('app_jobs.job_queues_locked_by_idx'::regclass, 'app_jobs.job_queues'::regclass); ROLLBACK; diff --git a/packages/database-jobs/verify/schemas/app_jobs/tables/job_queues/table.sql b/packages/database-jobs/verify/schemas/app_jobs/tables/job_queues/table.sql index 3a5e4b1cd..b63fc4af4 100644 --- a/packages/database-jobs/verify/schemas/app_jobs/tables/job_queues/table.sql +++ b/packages/database-jobs/verify/schemas/app_jobs/tables/job_queues/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('app_jobs.job_queues'); +SELECT assert_table('app_jobs.job_queues'::regclass); ROLLBACK; diff --git a/packages/database-jobs/verify/schemas/app_jobs/tables/jobs/grants/grant_select_insert_update_delete_to_administrator.sql b/packages/database-jobs/verify/schemas/app_jobs/tables/jobs/grants/grant_select_insert_update_delete_to_administrator.sql index 6255d7164..f2da0c696 100644 --- a/packages/database-jobs/verify/schemas/app_jobs/tables/jobs/grants/grant_select_insert_update_delete_to_administrator.sql +++ b/packages/database-jobs/verify/schemas/app_jobs/tables/jobs/grants/grant_select_insert_update_delete_to_administrator.sql @@ -2,9 +2,9 @@ BEGIN; - SELECT has_table_privilege('administrator', 'app_jobs.jobs', 'SELECT'); - SELECT has_table_privilege('administrator', 'app_jobs.jobs', 'INSERT'); - SELECT has_table_privilege('administrator', 'app_jobs.jobs', 'UPDATE'); - SELECT has_table_privilege('administrator', 'app_jobs.jobs', 'DELETE'); + SELECT assert_table_grant('app_jobs.jobs'::regclass, 'administrator', 'SELECT'); + SELECT assert_table_grant('app_jobs.jobs'::regclass, 'administrator', 'INSERT'); + SELECT assert_table_grant('app_jobs.jobs'::regclass, 'administrator', 'UPDATE'); + SELECT assert_table_grant('app_jobs.jobs'::regclass, 'administrator', 'DELETE'); ROLLBACK; diff --git a/packages/database-jobs/verify/schemas/app_jobs/tables/jobs/indexes/jobs_locked_by_idx.sql b/packages/database-jobs/verify/schemas/app_jobs/tables/jobs/indexes/jobs_locked_by_idx.sql index 3635677ae..b6d72c45c 100644 --- a/packages/database-jobs/verify/schemas/app_jobs/tables/jobs/indexes/jobs_locked_by_idx.sql +++ b/packages/database-jobs/verify/schemas/app_jobs/tables/jobs/indexes/jobs_locked_by_idx.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_index ('app_jobs.jobs', 'jobs_locked_by_idx'); +SELECT assert_index('app_jobs.jobs_locked_by_idx'::regclass, 'app_jobs.jobs'::regclass); ROLLBACK; diff --git a/packages/database-jobs/verify/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql b/packages/database-jobs/verify/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql index eeec4f53d..533fd8859 100644 --- a/packages/database-jobs/verify/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql +++ b/packages/database-jobs/verify/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql @@ -2,7 +2,7 @@ BEGIN; -SELECT verify_index ('app_jobs.jobs', 'jobs_main_index'); -SELECT verify_index ('app_jobs.jobs', 'jobs_no_queue_index'); +SELECT assert_index('app_jobs.jobs_main_index'::regclass, 'app_jobs.jobs'::regclass); +SELECT assert_index('app_jobs.jobs_no_queue_index'::regclass, 'app_jobs.jobs'::regclass); ROLLBACK; diff --git a/packages/database-jobs/verify/schemas/app_jobs/tables/jobs/table.sql b/packages/database-jobs/verify/schemas/app_jobs/tables/jobs/table.sql index aaa0584da..30c45d200 100644 --- a/packages/database-jobs/verify/schemas/app_jobs/tables/jobs/table.sql +++ b/packages/database-jobs/verify/schemas/app_jobs/tables/jobs/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('app_jobs.jobs'); +SELECT assert_table('app_jobs.jobs'::regclass); ROLLBACK; diff --git a/packages/database-jobs/verify/schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count.sql b/packages/database-jobs/verify/schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count.sql index 97b717d08..bf960d874 100644 --- a/packages/database-jobs/verify/schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count.sql +++ b/packages/database-jobs/verify/schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count.sql @@ -1,10 +1,23 @@ -- Verify schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count on pg + BEGIN; -SELECT - verify_function ('app_jobs.tg_decrease_job_queue_count'); -SELECT - verify_trigger ('app_jobs.decrease_job_queue_count_on_delete'); -SELECT - verify_trigger ('app_jobs.decrease_job_queue_count_on_update'); -ROLLBACK; +SELECT assert_function('app_jobs.tg_decrease_job_queue_count()'::regprocedure, 'trigger'::regtype); + +-- AFTER (0) DELETE (8) FOR EACH ROW (1). +SELECT assert_trigger( + 'app_jobs.jobs'::regclass, + 'decrease_job_queue_count_on_delete', + 'app_jobs.tg_decrease_job_queue_count'::regproc, + 9 +); + +-- AFTER (0) UPDATE (16) FOR EACH ROW (1). +SELECT assert_trigger( + 'app_jobs.jobs'::regclass, + 'decrease_job_queue_count_on_update', + 'app_jobs.tg_decrease_job_queue_count'::regproc, + 17 +); + +ROLLBACK; diff --git a/packages/database-jobs/verify/schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count.sql b/packages/database-jobs/verify/schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count.sql index a6e89dd17..d84e959e4 100644 --- a/packages/database-jobs/verify/schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count.sql +++ b/packages/database-jobs/verify/schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count.sql @@ -1,10 +1,23 @@ -- Verify schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count on pg + BEGIN; -SELECT - verify_function ('app_jobs.tg_increase_job_queue_count'); -SELECT - verify_trigger ('app_jobs._500_increase_job_queue_count_on_insert'); -SELECT - verify_trigger ('app_jobs._500_increase_job_queue_count_on_update'); -ROLLBACK; +SELECT assert_function('app_jobs.tg_increase_job_queue_count()'::regprocedure, 'trigger'::regtype); + +-- AFTER (0) INSERT (4) FOR EACH ROW (1). +SELECT assert_trigger( + 'app_jobs.jobs'::regclass, + '_500_increase_job_queue_count_on_insert', + 'app_jobs.tg_increase_job_queue_count'::regproc, + 5 +); + +-- AFTER (0) UPDATE (16) FOR EACH ROW (1). +SELECT assert_trigger( + 'app_jobs.jobs'::regclass, + '_500_increase_job_queue_count_on_update', + 'app_jobs.tg_increase_job_queue_count'::regproc, + 17 +); + +ROLLBACK; diff --git a/packages/database-jobs/verify/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql b/packages/database-jobs/verify/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql index 96a054a02..804f381a2 100644 --- a/packages/database-jobs/verify/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql +++ b/packages/database-jobs/verify/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql @@ -1,5 +1,15 @@ -- Verify schemas/app_jobs/tables/jobs/triggers/notify_worker on pg + BEGIN; -SELECT - verify_trigger ('app_jobs._900_after_insert'); + +SELECT assert_function('app_jobs.tg_jobs__after_insert()'::regprocedure, 'trigger'::regtype); + +-- AFTER (0) INSERT (4) FOR EACH STATEMENT (0). +SELECT assert_trigger( + 'app_jobs.jobs'::regclass, + '_900_after_insert', + 'app_jobs.tg_jobs__after_insert'::regproc, + 4 +); + ROLLBACK; diff --git a/packages/database-jobs/verify/schemas/app_jobs/tables/jobs/triggers/timestamps.sql b/packages/database-jobs/verify/schemas/app_jobs/tables/jobs/triggers/timestamps.sql index ed9466a37..fd7bac1c4 100644 --- a/packages/database-jobs/verify/schemas/app_jobs/tables/jobs/triggers/timestamps.sql +++ b/packages/database-jobs/verify/schemas/app_jobs/tables/jobs/triggers/timestamps.sql @@ -1,16 +1,25 @@ -- Verify schemas/app_jobs/tables/jobs/triggers/timestamps on pg + BEGIN; + SELECT created_at FROM app_jobs.jobs LIMIT 1; + SELECT updated_at FROM app_jobs.jobs LIMIT 1; -SELECT - verify_trigger ('app_jobs._100_update_jobs_modtime_tg'); -ROLLBACK; +-- BEFORE (2) INSERT (4) OR UPDATE (16) FOR EACH ROW (1). +SELECT assert_trigger( + 'app_jobs.jobs'::regclass, + '_100_update_jobs_modtime_tg', + 'app_jobs.tg_update_timestamps'::regproc, + 23 +); + +ROLLBACK; diff --git a/packages/database-jobs/verify/schemas/app_jobs/tables/scheduled_jobs/grants/grant_select_insert_update_delete_to_administrator.sql b/packages/database-jobs/verify/schemas/app_jobs/tables/scheduled_jobs/grants/grant_select_insert_update_delete_to_administrator.sql index c4aa4eb6c..781fee1a1 100644 --- a/packages/database-jobs/verify/schemas/app_jobs/tables/scheduled_jobs/grants/grant_select_insert_update_delete_to_administrator.sql +++ b/packages/database-jobs/verify/schemas/app_jobs/tables/scheduled_jobs/grants/grant_select_insert_update_delete_to_administrator.sql @@ -2,9 +2,9 @@ BEGIN; - SELECT has_table_privilege('administrator', 'app_jobs.scheduled_jobs', 'SELECT'); - SELECT has_table_privilege('administrator', 'app_jobs.scheduled_jobs', 'INSERT'); - SELECT has_table_privilege('administrator', 'app_jobs.scheduled_jobs', 'UPDATE'); - SELECT has_table_privilege('administrator', 'app_jobs.scheduled_jobs', 'DELETE'); + SELECT assert_table_grant('app_jobs.scheduled_jobs'::regclass, 'administrator', 'SELECT'); + SELECT assert_table_grant('app_jobs.scheduled_jobs'::regclass, 'administrator', 'INSERT'); + SELECT assert_table_grant('app_jobs.scheduled_jobs'::regclass, 'administrator', 'UPDATE'); + SELECT assert_table_grant('app_jobs.scheduled_jobs'::regclass, 'administrator', 'DELETE'); ROLLBACK; diff --git a/packages/database-jobs/verify/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_locked_by_idx.sql b/packages/database-jobs/verify/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_locked_by_idx.sql index 34ee9f117..0daf6e4ca 100644 --- a/packages/database-jobs/verify/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_locked_by_idx.sql +++ b/packages/database-jobs/verify/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_locked_by_idx.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_index ('app_jobs.scheduled_jobs', 'scheduled_jobs_locked_by_idx'); +SELECT assert_index('app_jobs.scheduled_jobs_locked_by_idx'::regclass, 'app_jobs.scheduled_jobs'::regclass); ROLLBACK; diff --git a/packages/database-jobs/verify/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_priority_id_idx.sql b/packages/database-jobs/verify/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_priority_id_idx.sql index d26a68223..3f98b247f 100644 --- a/packages/database-jobs/verify/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_priority_id_idx.sql +++ b/packages/database-jobs/verify/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_priority_id_idx.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_index ('app_jobs.scheduled_jobs', 'scheduled_jobs_priority_id_idx'); +SELECT assert_index('app_jobs.scheduled_jobs_priority_id_idx'::regclass, 'app_jobs.scheduled_jobs'::regclass); ROLLBACK; diff --git a/packages/database-jobs/verify/schemas/app_jobs/tables/scheduled_jobs/table.sql b/packages/database-jobs/verify/schemas/app_jobs/tables/scheduled_jobs/table.sql index 065f427b1..0966ef07f 100644 --- a/packages/database-jobs/verify/schemas/app_jobs/tables/scheduled_jobs/table.sql +++ b/packages/database-jobs/verify/schemas/app_jobs/tables/scheduled_jobs/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('app_jobs.scheduled_jobs'); +SELECT assert_table('app_jobs.scheduled_jobs'::regclass); ROLLBACK; diff --git a/packages/database-jobs/verify/schemas/app_jobs/tables/scheduled_jobs/triggers/notify_scheduled_job.sql b/packages/database-jobs/verify/schemas/app_jobs/tables/scheduled_jobs/triggers/notify_scheduled_job.sql index 599c63a3a..13559df1c 100644 --- a/packages/database-jobs/verify/schemas/app_jobs/tables/scheduled_jobs/triggers/notify_scheduled_job.sql +++ b/packages/database-jobs/verify/schemas/app_jobs/tables/scheduled_jobs/triggers/notify_scheduled_job.sql @@ -3,6 +3,6 @@ BEGIN; -SELECT verify_trigger ('app_jobs._900_notify_scheduled_job'); +SELECT assert_trigger('app_jobs.scheduled_jobs'::regclass, '_900_notify_scheduled_job', 'app_jobs.do_notify'::regproc, 5); ROLLBACK; diff --git a/packages/database-jobs/verify/schemas/app_jobs/triggers/tg_add_job_with_fields.sql b/packages/database-jobs/verify/schemas/app_jobs/triggers/tg_add_job_with_fields.sql index 9b36e4f2e..aa3af3205 100644 --- a/packages/database-jobs/verify/schemas/app_jobs/triggers/tg_add_job_with_fields.sql +++ b/packages/database-jobs/verify/schemas/app_jobs/triggers/tg_add_job_with_fields.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('app_jobs.trigger_job_with_fields'); +SELECT assert_function('app_jobs.trigger_job_with_fields()'::regprocedure); ROLLBACK; diff --git a/packages/database-jobs/verify/schemas/app_jobs/triggers/tg_add_job_with_row.sql b/packages/database-jobs/verify/schemas/app_jobs/triggers/tg_add_job_with_row.sql index bdf8cc7eb..cf472d3fc 100644 --- a/packages/database-jobs/verify/schemas/app_jobs/triggers/tg_add_job_with_row.sql +++ b/packages/database-jobs/verify/schemas/app_jobs/triggers/tg_add_job_with_row.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('app_jobs.tg_add_job_with_row'); +SELECT assert_function('app_jobs.tg_add_job_with_row()'::regprocedure); ROLLBACK; diff --git a/packages/database-jobs/verify/schemas/app_jobs/triggers/tg_add_job_with_row_id.sql b/packages/database-jobs/verify/schemas/app_jobs/triggers/tg_add_job_with_row_id.sql index 72b5a7b90..f64627ea4 100644 --- a/packages/database-jobs/verify/schemas/app_jobs/triggers/tg_add_job_with_row_id.sql +++ b/packages/database-jobs/verify/schemas/app_jobs/triggers/tg_add_job_with_row_id.sql @@ -1,6 +1,7 @@ -- Verify schemas/app_jobs/triggers/tg_add_job_with_row_id on pg + BEGIN; -SELECT - verify_function ('app_jobs.tg_add_job_with_row_id'); -ROLLBACK; +SELECT assert_function('app_jobs.tg_add_job_with_row_id()'::regprocedure, 'trigger'::regtype); + +ROLLBACK; diff --git a/packages/database-jobs/verify/schemas/app_jobs/triggers/tg_update_timestamps.sql b/packages/database-jobs/verify/schemas/app_jobs/triggers/tg_update_timestamps.sql index fd53ed3e9..16fee810a 100644 --- a/packages/database-jobs/verify/schemas/app_jobs/triggers/tg_update_timestamps.sql +++ b/packages/database-jobs/verify/schemas/app_jobs/triggers/tg_update_timestamps.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('app_jobs.tg_update_timestamps'); +SELECT assert_function('app_jobs.tg_update_timestamps()'::regprocedure); ROLLBACK; diff --git a/packages/defaults/Makefile b/packages/defaults/Makefile index 74ee5a020..cf8d215a1 100644 --- a/packages/defaults/Makefile +++ b/packages/defaults/Makefile @@ -1,5 +1,5 @@ EXTENSION = pgpm-defaults -DATA = sql/pgpm-defaults--0.36.0.sql +DATA = sql/pgpm-defaults--0.39.0.sql PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) diff --git a/packages/defaults/package.json b/packages/defaults/package.json index 30f2ce07a..9457bcf37 100644 --- a/packages/defaults/package.json +++ b/packages/defaults/package.json @@ -1,6 +1,6 @@ { "name": "@pgpm/defaults", - "version": "0.36.0", + "version": "0.39.0", "description": "Security defaults and baseline configurations", "author": "Dan Lynch ", "contributors": [ diff --git a/packages/defaults/pgpm-defaults.control b/packages/defaults/pgpm-defaults.control index 841ae1073..01d5000fb 100644 --- a/packages/defaults/pgpm-defaults.control +++ b/packages/defaults/pgpm-defaults.control @@ -1,6 +1,6 @@ # pgpm-defaults extension comment = 'pgpm-defaults extension' -default_version = '0.36.0' +default_version = '0.39.0' module_pathname = '$libdir/pgpm-defaults' requires = 'plpgsql,pgpm-verify' relocatable = false diff --git a/packages/defaults/sql/pgpm-defaults--0.36.0.bundle.tar.gz b/packages/defaults/sql/pgpm-defaults--0.36.0.bundle.tar.gz deleted file mode 100644 index 897c85838..000000000 Binary files a/packages/defaults/sql/pgpm-defaults--0.36.0.bundle.tar.gz and /dev/null differ diff --git a/packages/defaults/sql/pgpm-defaults--0.39.0.bundle.tar.gz b/packages/defaults/sql/pgpm-defaults--0.39.0.bundle.tar.gz new file mode 100644 index 000000000..3c13bd11f Binary files /dev/null and b/packages/defaults/sql/pgpm-defaults--0.39.0.bundle.tar.gz differ diff --git a/packages/defaults/sql/pgpm-defaults--0.36.0.sql b/packages/defaults/sql/pgpm-defaults--0.39.0.sql similarity index 100% rename from packages/defaults/sql/pgpm-defaults--0.36.0.sql rename to packages/defaults/sql/pgpm-defaults--0.39.0.sql diff --git a/packages/encrypted-secrets-table/Makefile b/packages/encrypted-secrets-table/Makefile index 4e39b8110..c321623e1 100644 --- a/packages/encrypted-secrets-table/Makefile +++ b/packages/encrypted-secrets-table/Makefile @@ -1,5 +1,5 @@ EXTENSION = pgpm-encrypted-secrets-table -DATA = sql/pgpm-encrypted-secrets-table--0.36.0.sql +DATA = sql/pgpm-encrypted-secrets-table--0.39.0.sql PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) diff --git a/packages/encrypted-secrets-table/package.json b/packages/encrypted-secrets-table/package.json index dc7dd12b1..2043d34a8 100644 --- a/packages/encrypted-secrets-table/package.json +++ b/packages/encrypted-secrets-table/package.json @@ -1,6 +1,6 @@ { "name": "@pgpm/encrypted-secrets-table", - "version": "0.36.0", + "version": "0.39.0", "description": "Table-based encrypted secrets storage and retrieval", "author": "Dan Lynch ", "contributors": [ diff --git a/packages/encrypted-secrets-table/pgpm-encrypted-secrets-table.control b/packages/encrypted-secrets-table/pgpm-encrypted-secrets-table.control index 236003264..d2524bf73 100644 --- a/packages/encrypted-secrets-table/pgpm-encrypted-secrets-table.control +++ b/packages/encrypted-secrets-table/pgpm-encrypted-secrets-table.control @@ -1,6 +1,6 @@ # pgpm-encrypted-secrets-table extension comment = 'pgpm-encrypted-secrets-table extension' -default_version = '0.36.0' +default_version = '0.39.0' module_pathname = '$libdir/pgpm-encrypted-secrets-table' requires = 'pgcrypto,plpgsql,pgpm-verify' relocatable = false diff --git a/packages/encrypted-secrets-table/revert/schemas/secrets_schema/tables/secrets_table/triggers/hash_secrets.sql b/packages/encrypted-secrets-table/revert/schemas/secrets_schema/tables/secrets_table/triggers/hash_secrets.sql index 6459a0931..38e8e1f84 100644 --- a/packages/encrypted-secrets-table/revert/schemas/secrets_schema/tables/secrets_table/triggers/hash_secrets.sql +++ b/packages/encrypted-secrets-table/revert/schemas/secrets_schema/tables/secrets_table/triggers/hash_secrets.sql @@ -2,8 +2,8 @@ BEGIN; -DROP TRIGGER hash_secrets_update ON secrets_schema.secrets_table; DROP TRIGGER hash_secrets_insert ON secrets_schema.secrets_table; -DROP FUNCTION secrets_schema.tg_hash_secrets; +DROP TRIGGER hash_secrets_update ON secrets_schema.secrets_table; +DROP FUNCTION secrets_schema.tg_hash_secrets(); COMMIT; diff --git a/packages/encrypted-secrets-table/sql/pgpm-encrypted-secrets-table--0.36.0.bundle.tar.gz b/packages/encrypted-secrets-table/sql/pgpm-encrypted-secrets-table--0.36.0.bundle.tar.gz deleted file mode 100644 index dd53643e7..000000000 Binary files a/packages/encrypted-secrets-table/sql/pgpm-encrypted-secrets-table--0.36.0.bundle.tar.gz and /dev/null differ diff --git a/packages/encrypted-secrets-table/sql/pgpm-encrypted-secrets-table--0.39.0.bundle.tar.gz b/packages/encrypted-secrets-table/sql/pgpm-encrypted-secrets-table--0.39.0.bundle.tar.gz new file mode 100644 index 000000000..c75e57682 Binary files /dev/null and b/packages/encrypted-secrets-table/sql/pgpm-encrypted-secrets-table--0.39.0.bundle.tar.gz differ diff --git a/packages/encrypted-secrets-table/sql/pgpm-encrypted-secrets-table--0.36.0.sql b/packages/encrypted-secrets-table/sql/pgpm-encrypted-secrets-table--0.39.0.sql similarity index 100% rename from packages/encrypted-secrets-table/sql/pgpm-encrypted-secrets-table--0.36.0.sql rename to packages/encrypted-secrets-table/sql/pgpm-encrypted-secrets-table--0.39.0.sql diff --git a/packages/encrypted-secrets-table/verify/schemas/secrets_schema/schema.sql b/packages/encrypted-secrets-table/verify/schemas/secrets_schema/schema.sql index 092347b0f..7e57b210b 100644 --- a/packages/encrypted-secrets-table/verify/schemas/secrets_schema/schema.sql +++ b/packages/encrypted-secrets-table/verify/schemas/secrets_schema/schema.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_schema ('secrets_schema'); +SELECT assert_schema('secrets_schema'::regnamespace); ROLLBACK; diff --git a/packages/encrypted-secrets-table/verify/schemas/secrets_schema/tables/secrets_table/table.sql b/packages/encrypted-secrets-table/verify/schemas/secrets_schema/tables/secrets_table/table.sql index d4d3f0fd0..2ad7f2d45 100644 --- a/packages/encrypted-secrets-table/verify/schemas/secrets_schema/tables/secrets_table/table.sql +++ b/packages/encrypted-secrets-table/verify/schemas/secrets_schema/tables/secrets_table/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('secrets_schema.secrets_table'); +SELECT assert_table('secrets_schema.secrets_table'::regclass); ROLLBACK; diff --git a/packages/encrypted-secrets-table/verify/schemas/secrets_schema/tables/secrets_table/triggers/hash_secrets.sql b/packages/encrypted-secrets-table/verify/schemas/secrets_schema/tables/secrets_table/triggers/hash_secrets.sql index 63d1690d7..0eb6e97f5 100644 --- a/packages/encrypted-secrets-table/verify/schemas/secrets_schema/tables/secrets_table/triggers/hash_secrets.sql +++ b/packages/encrypted-secrets-table/verify/schemas/secrets_schema/tables/secrets_table/triggers/hash_secrets.sql @@ -2,8 +2,8 @@ BEGIN; -SELECT verify_function ('secrets_schema.tg_hash_secrets'); -SELECT verify_trigger ('secrets_schema.hash_secrets_update'); -SELECT verify_trigger ('secrets_schema.hash_secrets_insert'); +SELECT assert_function('secrets_schema.tg_hash_secrets()'::regprocedure); +SELECT assert_trigger('secrets_schema.secrets_table'::regclass, 'hash_secrets_update', 'secrets_schema.tg_hash_secrets'::regproc, 19); +SELECT assert_trigger('secrets_schema.secrets_table'::regclass, 'hash_secrets_insert', 'secrets_schema.tg_hash_secrets'::regproc, 7); ROLLBACK; diff --git a/packages/encrypted-secrets/Makefile b/packages/encrypted-secrets/Makefile index 46733e537..cb574cde9 100644 --- a/packages/encrypted-secrets/Makefile +++ b/packages/encrypted-secrets/Makefile @@ -1,5 +1,5 @@ EXTENSION = pgpm-encrypted-secrets -DATA = sql/pgpm-encrypted-secrets--0.36.0.sql +DATA = sql/pgpm-encrypted-secrets--0.39.0.sql PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) diff --git a/packages/encrypted-secrets/package.json b/packages/encrypted-secrets/package.json index e6694c7b1..4af832e34 100644 --- a/packages/encrypted-secrets/package.json +++ b/packages/encrypted-secrets/package.json @@ -1,6 +1,6 @@ { "name": "@pgpm/encrypted-secrets", - "version": "0.36.0", + "version": "0.39.0", "description": "Encrypted secrets management for PostgreSQL", "author": "Dan Lynch ", "contributors": [ diff --git a/packages/encrypted-secrets/pgpm-encrypted-secrets.control b/packages/encrypted-secrets/pgpm-encrypted-secrets.control index 395af4acc..ac8d5c307 100644 --- a/packages/encrypted-secrets/pgpm-encrypted-secrets.control +++ b/packages/encrypted-secrets/pgpm-encrypted-secrets.control @@ -1,6 +1,6 @@ # pgpm-encrypted-secrets extension comment = 'pgpm-encrypted-secrets extension' -default_version = '0.36.0' +default_version = '0.39.0' module_pathname = '$libdir/pgpm-encrypted-secrets' requires = 'pgcrypto,plpgsql,pgpm-encrypted-secrets-table,pgpm-verify' relocatable = false diff --git a/packages/encrypted-secrets/revert/schemas/encrypted_secrets/procedures/encrypt_field_bytea_to_text.sql b/packages/encrypted-secrets/revert/schemas/encrypted_secrets/procedures/encrypt_field_bytea_to_text.sql index a2c7cfce0..506892eb1 100644 --- a/packages/encrypted-secrets/revert/schemas/encrypted_secrets/procedures/encrypt_field_bytea_to_text.sql +++ b/packages/encrypted-secrets/revert/schemas/encrypted_secrets/procedures/encrypt_field_bytea_to_text.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION encrypted_secrets.encrypt_field_bytea_to_text; +DROP FUNCTION encrypted_secrets.encrypt_field_bytea_to_text(bytea); COMMIT; diff --git a/packages/encrypted-secrets/revert/schemas/encrypted_secrets/procedures/encrypt_field_crypt.sql b/packages/encrypted-secrets/revert/schemas/encrypted_secrets/procedures/encrypt_field_crypt.sql index bba249384..cb7646b96 100644 --- a/packages/encrypted-secrets/revert/schemas/encrypted_secrets/procedures/encrypt_field_crypt.sql +++ b/packages/encrypted-secrets/revert/schemas/encrypted_secrets/procedures/encrypt_field_crypt.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION encrypted_secrets.encrypt_field_crypt; +DROP FUNCTION encrypted_secrets.encrypt_field_crypt(); COMMIT; diff --git a/packages/encrypted-secrets/revert/schemas/encrypted_secrets/procedures/encrypt_field_crypt_verify.sql b/packages/encrypted-secrets/revert/schemas/encrypted_secrets/procedures/encrypt_field_crypt_verify.sql index 07402fe40..07e616167 100644 --- a/packages/encrypted-secrets/revert/schemas/encrypted_secrets/procedures/encrypt_field_crypt_verify.sql +++ b/packages/encrypted-secrets/revert/schemas/encrypted_secrets/procedures/encrypt_field_crypt_verify.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION encrypted_secrets.encrypt_field_crypt_verify; +DROP FUNCTION encrypted_secrets.encrypt_field_crypt_verify(uuid, text, text); COMMIT; diff --git a/packages/encrypted-secrets/revert/schemas/encrypted_secrets/procedures/encrypt_field_pgp.sql b/packages/encrypted-secrets/revert/schemas/encrypted_secrets/procedures/encrypt_field_pgp.sql index 7d186482c..3c4e46af5 100644 --- a/packages/encrypted-secrets/revert/schemas/encrypted_secrets/procedures/encrypt_field_pgp.sql +++ b/packages/encrypted-secrets/revert/schemas/encrypted_secrets/procedures/encrypt_field_pgp.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION encrypted_secrets.encrypt_field_pgp; +DROP FUNCTION encrypted_secrets.encrypt_field_pgp(); COMMIT; diff --git a/packages/encrypted-secrets/revert/schemas/encrypted_secrets/procedures/encrypt_field_pgp_get.sql b/packages/encrypted-secrets/revert/schemas/encrypted_secrets/procedures/encrypt_field_pgp_get.sql index f3933b493..e6409fbb0 100644 --- a/packages/encrypted-secrets/revert/schemas/encrypted_secrets/procedures/encrypt_field_pgp_get.sql +++ b/packages/encrypted-secrets/revert/schemas/encrypted_secrets/procedures/encrypt_field_pgp_get.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION encrypted_secrets.encrypt_field_pgp_get; +DROP FUNCTION encrypted_secrets.encrypt_field_pgp_get(bytea, text); COMMIT; diff --git a/packages/encrypted-secrets/revert/schemas/encrypted_secrets/procedures/encrypt_field_pgp_getter.sql b/packages/encrypted-secrets/revert/schemas/encrypted_secrets/procedures/encrypt_field_pgp_getter.sql index c5a140e74..fc6db03f7 100644 --- a/packages/encrypted-secrets/revert/schemas/encrypted_secrets/procedures/encrypt_field_pgp_getter.sql +++ b/packages/encrypted-secrets/revert/schemas/encrypted_secrets/procedures/encrypt_field_pgp_getter.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION encrypted_secrets.encrypt_field_pgp_getter; +DROP FUNCTION encrypted_secrets.encrypt_field_pgp_getter(uuid, text, text); COMMIT; diff --git a/packages/encrypted-secrets/revert/schemas/encrypted_secrets/procedures/encrypt_field_set.sql b/packages/encrypted-secrets/revert/schemas/encrypted_secrets/procedures/encrypt_field_set.sql index 31070214e..3221a4604 100644 --- a/packages/encrypted-secrets/revert/schemas/encrypted_secrets/procedures/encrypt_field_set.sql +++ b/packages/encrypted-secrets/revert/schemas/encrypted_secrets/procedures/encrypt_field_set.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION encrypted_secrets.encrypt_field_set; +DROP FUNCTION encrypted_secrets.encrypt_field_set(text); COMMIT; diff --git a/packages/encrypted-secrets/revert/schemas/encrypted_secrets/procedures/secrets_getter.sql b/packages/encrypted-secrets/revert/schemas/encrypted_secrets/procedures/secrets_getter.sql index c51010ad9..fa8c4238f 100644 --- a/packages/encrypted-secrets/revert/schemas/encrypted_secrets/procedures/secrets_getter.sql +++ b/packages/encrypted-secrets/revert/schemas/encrypted_secrets/procedures/secrets_getter.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION encrypted_secrets.secrets_getter; +DROP FUNCTION encrypted_secrets.secrets_getter(uuid, text, text); COMMIT; diff --git a/packages/encrypted-secrets/revert/schemas/encrypted_secrets/procedures/secrets_table_upsert.sql b/packages/encrypted-secrets/revert/schemas/encrypted_secrets/procedures/secrets_table_upsert.sql index 61f670699..c16304343 100644 --- a/packages/encrypted-secrets/revert/schemas/encrypted_secrets/procedures/secrets_table_upsert.sql +++ b/packages/encrypted-secrets/revert/schemas/encrypted_secrets/procedures/secrets_table_upsert.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION encrypted_secrets.secrets_table_upsert; +DROP FUNCTION encrypted_secrets.secrets_table_upsert(uuid, json); COMMIT; diff --git a/packages/encrypted-secrets/revert/schemas/encrypted_secrets/procedures/secrets_upsert.sql b/packages/encrypted-secrets/revert/schemas/encrypted_secrets/procedures/secrets_upsert.sql index e9d350b77..d43062ef2 100644 --- a/packages/encrypted-secrets/revert/schemas/encrypted_secrets/procedures/secrets_upsert.sql +++ b/packages/encrypted-secrets/revert/schemas/encrypted_secrets/procedures/secrets_upsert.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION encrypted_secrets.secrets_upsert; +DROP FUNCTION encrypted_secrets.secrets_upsert(uuid, text, text, text); COMMIT; diff --git a/packages/encrypted-secrets/revert/schemas/encrypted_secrets/procedures/secrets_verify.sql b/packages/encrypted-secrets/revert/schemas/encrypted_secrets/procedures/secrets_verify.sql index 01e34d7c6..7ee60bb24 100644 --- a/packages/encrypted-secrets/revert/schemas/encrypted_secrets/procedures/secrets_verify.sql +++ b/packages/encrypted-secrets/revert/schemas/encrypted_secrets/procedures/secrets_verify.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION encrypted_secrets.secrets_verify; +DROP FUNCTION encrypted_secrets.secrets_verify(uuid, text, text); COMMIT; diff --git a/packages/encrypted-secrets/sql/pgpm-encrypted-secrets--0.36.0.bundle.tar.gz b/packages/encrypted-secrets/sql/pgpm-encrypted-secrets--0.36.0.bundle.tar.gz deleted file mode 100644 index aef375467..000000000 Binary files a/packages/encrypted-secrets/sql/pgpm-encrypted-secrets--0.36.0.bundle.tar.gz and /dev/null differ diff --git a/packages/encrypted-secrets/sql/pgpm-encrypted-secrets--0.39.0.bundle.tar.gz b/packages/encrypted-secrets/sql/pgpm-encrypted-secrets--0.39.0.bundle.tar.gz new file mode 100644 index 000000000..80c25971b Binary files /dev/null and b/packages/encrypted-secrets/sql/pgpm-encrypted-secrets--0.39.0.bundle.tar.gz differ diff --git a/packages/encrypted-secrets/sql/pgpm-encrypted-secrets--0.36.0.sql b/packages/encrypted-secrets/sql/pgpm-encrypted-secrets--0.39.0.sql similarity index 100% rename from packages/encrypted-secrets/sql/pgpm-encrypted-secrets--0.36.0.sql rename to packages/encrypted-secrets/sql/pgpm-encrypted-secrets--0.39.0.sql diff --git a/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/encrypt_field_bytea_to_text.sql b/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/encrypt_field_bytea_to_text.sql index 9cde2ce4b..e08a949fc 100644 --- a/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/encrypt_field_bytea_to_text.sql +++ b/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/encrypt_field_bytea_to_text.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('encrypted_secrets.encrypt_field_bytea_to_text'); +SELECT assert_function('encrypted_secrets.encrypt_field_bytea_to_text(bytea)'::regprocedure); ROLLBACK; diff --git a/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/encrypt_field_crypt.sql b/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/encrypt_field_crypt.sql index 7f5af2bb6..65eb2c237 100644 --- a/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/encrypt_field_crypt.sql +++ b/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/encrypt_field_crypt.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('encrypted_secrets.encrypt_field_crypt'); +SELECT assert_function('encrypted_secrets.encrypt_field_crypt()'::regprocedure); ROLLBACK; diff --git a/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/encrypt_field_crypt_verify.sql b/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/encrypt_field_crypt_verify.sql index f31b36780..8fdde56f9 100644 --- a/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/encrypt_field_crypt_verify.sql +++ b/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/encrypt_field_crypt_verify.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('encrypted_secrets.encrypt_field_crypt_verify'); +SELECT assert_function('encrypted_secrets.encrypt_field_crypt_verify(uuid, text, text)'::regprocedure); ROLLBACK; diff --git a/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/encrypt_field_pgp.sql b/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/encrypt_field_pgp.sql index 2ba11fecb..7a36e7dec 100644 --- a/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/encrypt_field_pgp.sql +++ b/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/encrypt_field_pgp.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('encrypted_secrets.encrypt_field_pgp'); +SELECT assert_function('encrypted_secrets.encrypt_field_pgp()'::regprocedure); ROLLBACK; diff --git a/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/encrypt_field_pgp_get.sql b/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/encrypt_field_pgp_get.sql index 88b5a6a1b..ecfd3d126 100644 --- a/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/encrypt_field_pgp_get.sql +++ b/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/encrypt_field_pgp_get.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('encrypted_secrets.encrypt_field_pgp_get'); +SELECT assert_function('encrypted_secrets.encrypt_field_pgp_get(bytea, text)'::regprocedure); ROLLBACK; diff --git a/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/encrypt_field_pgp_getter.sql b/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/encrypt_field_pgp_getter.sql index 0c1d631fc..bb84efecd 100644 --- a/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/encrypt_field_pgp_getter.sql +++ b/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/encrypt_field_pgp_getter.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('encrypted_secrets.encrypt_field_pgp_getter'); +SELECT assert_function('encrypted_secrets.encrypt_field_pgp_getter(uuid, text, text)'::regprocedure); ROLLBACK; diff --git a/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/encrypt_field_set.sql b/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/encrypt_field_set.sql index 317afc863..19e962080 100644 --- a/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/encrypt_field_set.sql +++ b/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/encrypt_field_set.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('encrypted_secrets.encrypt_field_set'); +SELECT assert_function('encrypted_secrets.encrypt_field_set(text)'::regprocedure); ROLLBACK; diff --git a/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/secrets_delete.sql b/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/secrets_delete.sql index bd2b6a0ae..f14a40b67 100644 --- a/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/secrets_delete.sql +++ b/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/secrets_delete.sql @@ -2,6 +2,7 @@ BEGIN; -SELECT verify_function ('encrypted_secrets.secrets_delete'); +SELECT assert_function('encrypted_secrets.secrets_delete(uuid, text)'::regprocedure); +SELECT assert_function('encrypted_secrets.secrets_delete(uuid, text[])'::regprocedure); ROLLBACK; diff --git a/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/secrets_getter.sql b/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/secrets_getter.sql index b733371d1..ee7721b16 100644 --- a/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/secrets_getter.sql +++ b/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/secrets_getter.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('encrypted_secrets.secrets_getter'); +SELECT assert_function('encrypted_secrets.secrets_getter(uuid, text, text)'::regprocedure); ROLLBACK; diff --git a/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/secrets_table_upsert.sql b/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/secrets_table_upsert.sql index 41237d207..a0084c489 100644 --- a/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/secrets_table_upsert.sql +++ b/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/secrets_table_upsert.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('encrypted_secrets.secrets_table_upsert'); +SELECT assert_function('encrypted_secrets.secrets_table_upsert(uuid, json)'::regprocedure); ROLLBACK; diff --git a/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/secrets_upsert.sql b/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/secrets_upsert.sql index ceca48343..d9ba94c6d 100644 --- a/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/secrets_upsert.sql +++ b/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/secrets_upsert.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('encrypted_secrets.secrets_upsert'); +SELECT assert_function('encrypted_secrets.secrets_upsert(uuid, text, text, text)'::regprocedure); ROLLBACK; diff --git a/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/secrets_verify.sql b/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/secrets_verify.sql index 709b6de69..dde9dac75 100644 --- a/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/secrets_verify.sql +++ b/packages/encrypted-secrets/verify/schemas/encrypted_secrets/procedures/secrets_verify.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('encrypted_secrets.secrets_verify'); +SELECT assert_function('encrypted_secrets.secrets_verify(uuid, text, text)'::regprocedure); ROLLBACK; diff --git a/packages/encrypted-secrets/verify/schemas/encrypted_secrets/schema.sql b/packages/encrypted-secrets/verify/schemas/encrypted_secrets/schema.sql index bd2071348..fffd99631 100644 --- a/packages/encrypted-secrets/verify/schemas/encrypted_secrets/schema.sql +++ b/packages/encrypted-secrets/verify/schemas/encrypted_secrets/schema.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_schema ('encrypted_secrets'); +SELECT assert_schema('encrypted_secrets'::regnamespace); ROLLBACK; diff --git a/packages/errors/Makefile b/packages/errors/Makefile index d03cde221..5bb30fb28 100644 --- a/packages/errors/Makefile +++ b/packages/errors/Makefile @@ -1,5 +1,5 @@ EXTENSION = errors -DATA = sql/errors--0.36.0.sql +DATA = sql/errors--0.39.0.sql PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) diff --git a/packages/errors/errors.control b/packages/errors/errors.control index 9f58b1884..d11a523d0 100644 --- a/packages/errors/errors.control +++ b/packages/errors/errors.control @@ -1,6 +1,6 @@ # errors extension comment = 'Canonical structured error raising for constructive-db' -default_version = '0.36.0' +default_version = '0.39.0' module_pathname = '$libdir/errors' requires = 'plpgsql' relocatable = false diff --git a/packages/errors/package.json b/packages/errors/package.json index d793f1510..1295502f4 100644 --- a/packages/errors/package.json +++ b/packages/errors/package.json @@ -1,6 +1,6 @@ { "name": "@pgpm/errors", - "version": "0.36.0", + "version": "0.39.0", "description": "Canonical structured error raising for PostgreSQL (errors.raise_error)", "author": "Dan Lynch ", "contributors": [ diff --git a/packages/errors/sql/errors--0.36.0.bundle.tar.gz b/packages/errors/sql/errors--0.36.0.bundle.tar.gz deleted file mode 100644 index 106a6f292..000000000 Binary files a/packages/errors/sql/errors--0.36.0.bundle.tar.gz and /dev/null differ diff --git a/packages/errors/sql/errors--0.39.0.bundle.tar.gz b/packages/errors/sql/errors--0.39.0.bundle.tar.gz new file mode 100644 index 000000000..5241c4603 Binary files /dev/null and b/packages/errors/sql/errors--0.39.0.bundle.tar.gz differ diff --git a/packages/errors/sql/errors--0.36.0.sql b/packages/errors/sql/errors--0.39.0.sql similarity index 100% rename from packages/errors/sql/errors--0.36.0.sql rename to packages/errors/sql/errors--0.39.0.sql diff --git a/packages/faker/Makefile b/packages/faker/Makefile index 61f09fc7f..f90e61e56 100644 --- a/packages/faker/Makefile +++ b/packages/faker/Makefile @@ -1,5 +1,5 @@ EXTENSION = pgpm-faker -DATA = sql/pgpm-faker--0.36.0.sql +DATA = sql/pgpm-faker--0.39.0.sql PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) diff --git a/packages/faker/package.json b/packages/faker/package.json index 2268dd3f3..67b8ef596 100644 --- a/packages/faker/package.json +++ b/packages/faker/package.json @@ -1,6 +1,6 @@ { "name": "@pgpm/faker", - "version": "0.36.0", + "version": "0.39.0", "description": "Fake data generation utilities for testing and development", "author": "Dan Lynch ", "contributors": [ diff --git a/packages/faker/pgpm-faker.control b/packages/faker/pgpm-faker.control index c8ef8fc15..f8ba515f2 100644 --- a/packages/faker/pgpm-faker.control +++ b/packages/faker/pgpm-faker.control @@ -1,6 +1,6 @@ # pgpm-faker extension comment = 'pgpm-faker extension' -default_version = '0.36.0' +default_version = '0.39.0' module_pathname = '$libdir/pgpm-faker' requires = 'citext,pgcrypto,plpgsql,pgpm-types,pgpm-verify' relocatable = false diff --git a/packages/faker/revert/schemas/faker/procedures/utils.sql b/packages/faker/revert/schemas/faker/procedures/utils.sql index b81d4b6e1..518e2facb 100644 --- a/packages/faker/revert/schemas/faker/procedures/utils.sql +++ b/packages/faker/revert/schemas/faker/procedures/utils.sql @@ -2,50 +2,50 @@ BEGIN; -DROP FUNCTION faker.attachment; -DROP FUNCTION faker.ip; -DROP FUNCTION faker.upload; -DROP FUNCTION faker.url; -DROP FUNCTION faker.file; -DROP FUNCTION faker.profilepic(gender text); +DROP FUNCTION faker.attachment(text); +DROP FUNCTION faker.ip(text); +DROP FUNCTION faker.upload(text); +DROP FUNCTION faker.url(text); +DROP FUNCTION faker.file(text); +DROP FUNCTION faker.profilepic(text); DROP FUNCTION faker.profilepic(); -DROP FUNCTION faker.image; -DROP FUNCTION faker.image_mime; -DROP FUNCTION faker.ext; -DROP FUNCTION faker.mime; -DROP FUNCTION faker.timestamptz; -DROP FUNCTION faker.boolean; -DROP FUNCTION faker.interval(min int, max int); +DROP FUNCTION faker.image(int4, int4); +DROP FUNCTION faker.image_mime(); +DROP FUNCTION faker.ext(text); +DROP FUNCTION faker.mime(); +DROP FUNCTION faker.timestamptz(bool); +DROP FUNCTION faker.boolean(); +DROP FUNCTION faker.interval(int4, int4); DROP FUNCTION faker.interval(); -DROP FUNCTION faker.birthdate; -DROP FUNCTION faker.date; -DROP FUNCTION faker.integer; -DROP FUNCTION faker.float; -DROP FUNCTION faker.time_unit; -DROP FUNCTION faker.hostname; -DROP FUNCTION faker.password; -DROP FUNCTION faker.token; -DROP FUNCTION faker.uuid; -DROP FUNCTION faker.email; -DROP FUNCTION faker.paragraph; -DROP FUNCTION faker.sentence; -DROP FUNCTION faker.tags; -DROP FUNCTION faker.address; -DROP FUNCTION faker.state; -DROP FUNCTION faker.street; -DROP FUNCTION faker.phone; -DROP FUNCTION faker.lnglat(city text); -DROP FUNCTION faker.lnglat(x1 float, y1 float, x2 float, y2 float); -DROP FUNCTION faker.zip; -DROP FUNCTION faker.city; -DROP FUNCTION faker.business; -DROP FUNCTION faker.fullname; -DROP FUNCTION faker.surname; -DROP FUNCTION faker.name; -DROP FUNCTION faker.username; -DROP FUNCTION faker.gender; -DROP FUNCTION faker.word(wordtypes text[]); -DROP FUNCTION faker.word(wordtype text); -DROP FUNCTION faker.word_type; +DROP FUNCTION faker.birthdate(int4, int4); +DROP FUNCTION faker.date(int4, int4, bool); +DROP FUNCTION faker.integer(int4, int4); +DROP FUNCTION faker.float(float8, float8); +DROP FUNCTION faker.time_unit(); +DROP FUNCTION faker.hostname(); +DROP FUNCTION faker.password(); +DROP FUNCTION faker.token(int4); +DROP FUNCTION faker.uuid(); +DROP FUNCTION faker.email(); +DROP FUNCTION faker.paragraph(text, int4, int4, text[]); +DROP FUNCTION faker.sentence(text, int4, int4, text[], text); +DROP FUNCTION faker.tags(int4, int4, text); +DROP FUNCTION faker.address(text, text); +DROP FUNCTION faker.state(text); +DROP FUNCTION faker.street(); +DROP FUNCTION faker.phone(); +DROP FUNCTION faker.lnglat(text); +DROP FUNCTION faker.lnglat(float8, float8, float8, float8); +DROP FUNCTION faker.zip(text); +DROP FUNCTION faker.city(text); +DROP FUNCTION faker.business(); +DROP FUNCTION faker.fullname(text); +DROP FUNCTION faker.surname(); +DROP FUNCTION faker.name(text); +DROP FUNCTION faker.username(); +DROP FUNCTION faker.gender(text); +DROP FUNCTION faker.word(text[]); +DROP FUNCTION faker.word(text); +DROP FUNCTION faker.word_type(); COMMIT; diff --git a/packages/faker/sql/pgpm-faker--0.36.0.bundle.tar.gz b/packages/faker/sql/pgpm-faker--0.36.0.bundle.tar.gz deleted file mode 100644 index a85f66c0a..000000000 Binary files a/packages/faker/sql/pgpm-faker--0.36.0.bundle.tar.gz and /dev/null differ diff --git a/packages/faker/sql/pgpm-faker--0.39.0.bundle.tar.gz b/packages/faker/sql/pgpm-faker--0.39.0.bundle.tar.gz new file mode 100644 index 000000000..bbecacd34 Binary files /dev/null and b/packages/faker/sql/pgpm-faker--0.39.0.bundle.tar.gz differ diff --git a/packages/faker/sql/pgpm-faker--0.36.0.sql b/packages/faker/sql/pgpm-faker--0.39.0.sql similarity index 100% rename from packages/faker/sql/pgpm-faker--0.36.0.sql rename to packages/faker/sql/pgpm-faker--0.39.0.sql diff --git a/packages/faker/verify/schemas/faker/procedures/utils.sql b/packages/faker/verify/schemas/faker/procedures/utils.sql index c6c3cd2c2..aad613af4 100644 --- a/packages/faker/verify/schemas/faker/procedures/utils.sql +++ b/packages/faker/verify/schemas/faker/procedures/utils.sql @@ -2,46 +2,50 @@ BEGIN; -SELECT verify_function ('faker.word_type'); -SELECT verify_function ('faker.word'); -SELECT verify_function ('faker.gender'); -SELECT verify_function ('faker.username'); -SELECT verify_function ('faker.name'); -SELECT verify_function ('faker.surname'); -SELECT verify_function ('faker.fullname'); -SELECT verify_function ('faker.business'); -SELECT verify_function ('faker.city'); -SELECT verify_function ('faker.zip'); -SELECT verify_function ('faker.lnglat'); -SELECT verify_function ('faker.phone'); -SELECT verify_function ('faker.street'); -SELECT verify_function ('faker.state'); -SELECT verify_function ('faker.address'); -SELECT verify_function ('faker.tags'); -SELECT verify_function ('faker.sentence'); -SELECT verify_function ('faker.paragraph'); -SELECT verify_function ('faker.email'); -SELECT verify_function ('faker.uuid'); -SELECT verify_function ('faker.token'); -SELECT verify_function ('faker.password'); -SELECT verify_function ('faker.hostname'); -SELECT verify_function ('faker.time_unit'); -SELECT verify_function ('faker.float'); -SELECT verify_function ('faker.integer'); -SELECT verify_function ('faker.date'); -SELECT verify_function ('faker.birthdate'); -SELECT verify_function ('faker.interval'); -SELECT verify_function ('faker.boolean'); -SELECT verify_function ('faker.timestamptz'); -SELECT verify_function ('faker.mime'); -SELECT verify_function ('faker.ext'); -SELECT verify_function ('faker.image_mime'); -SELECT verify_function ('faker.image'); -SELECT verify_function ('faker.profilepic'); -SELECT verify_function ('faker.file'); -SELECT verify_function ('faker.url'); -SELECT verify_function ('faker.upload'); -SELECT verify_function ('faker.ip'); -SELECT verify_function ('faker.attachment'); +SELECT assert_function('faker.word_type()'::regprocedure); +SELECT assert_function('faker.word(text)'::regprocedure); +SELECT assert_function('faker.word(text[])'::regprocedure); +SELECT assert_function('faker.gender(text)'::regprocedure); +SELECT assert_function('faker.username()'::regprocedure); +SELECT assert_function('faker.name(text)'::regprocedure); +SELECT assert_function('faker.surname()'::regprocedure); +SELECT assert_function('faker.fullname(text)'::regprocedure); +SELECT assert_function('faker.business()'::regprocedure); +SELECT assert_function('faker.city(text)'::regprocedure); +SELECT assert_function('faker.zip(text)'::regprocedure); +SELECT assert_function('faker.lnglat(float8, float8, float8, float8)'::regprocedure); +SELECT assert_function('faker.lnglat(text)'::regprocedure); +SELECT assert_function('faker.phone()'::regprocedure); +SELECT assert_function('faker.street()'::regprocedure); +SELECT assert_function('faker.state(text)'::regprocedure); +SELECT assert_function('faker.address(text, text)'::regprocedure); +SELECT assert_function('faker.tags(int4, int4, text)'::regprocedure); +SELECT assert_function('faker.sentence(text, int4, int4, text[], text)'::regprocedure); +SELECT assert_function('faker.paragraph(text, int4, int4, text[])'::regprocedure); +SELECT assert_function('faker.email()'::regprocedure); +SELECT assert_function('faker.uuid()'::regprocedure); +SELECT assert_function('faker.token(int4)'::regprocedure); +SELECT assert_function('faker.password()'::regprocedure); +SELECT assert_function('faker.hostname()'::regprocedure); +SELECT assert_function('faker.time_unit()'::regprocedure); +SELECT assert_function('faker.float(float8, float8)'::regprocedure); +SELECT assert_function('faker.integer(int4, int4)'::regprocedure); +SELECT assert_function('faker.date(int4, int4, bool)'::regprocedure); +SELECT assert_function('faker.birthdate(int4, int4)'::regprocedure); +SELECT assert_function('faker.interval()'::regprocedure); +SELECT assert_function('faker.interval(int4, int4)'::regprocedure); +SELECT assert_function('faker.boolean()'::regprocedure); +SELECT assert_function('faker.timestamptz(bool)'::regprocedure); +SELECT assert_function('faker.mime()'::regprocedure); +SELECT assert_function('faker.ext(text)'::regprocedure); +SELECT assert_function('faker.image_mime()'::regprocedure); +SELECT assert_function('faker.image(int4, int4)'::regprocedure); +SELECT assert_function('faker.profilepic()'::regprocedure); +SELECT assert_function('faker.profilepic(text)'::regprocedure); +SELECT assert_function('faker.file(text)'::regprocedure); +SELECT assert_function('faker.url(text)'::regprocedure); +SELECT assert_function('faker.upload(text)'::regprocedure); +SELECT assert_function('faker.ip(text)'::regprocedure); +SELECT assert_function('faker.attachment(text)'::regprocedure); ROLLBACK; diff --git a/packages/faker/verify/schemas/faker/schema.sql b/packages/faker/verify/schemas/faker/schema.sql index 526b3f9cc..92144e3c7 100644 --- a/packages/faker/verify/schemas/faker/schema.sql +++ b/packages/faker/verify/schemas/faker/schema.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_schema ('faker'); +SELECT assert_schema('faker'::regnamespace); ROLLBACK; diff --git a/packages/faker/verify/schemas/faker/tables/cities/table.sql b/packages/faker/verify/schemas/faker/tables/cities/table.sql index 0e728ed98..cedf0c465 100644 --- a/packages/faker/verify/schemas/faker/tables/cities/table.sql +++ b/packages/faker/verify/schemas/faker/tables/cities/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('faker.cities'); +SELECT assert_table('faker.cities'::regclass); ROLLBACK; diff --git a/packages/faker/verify/schemas/faker/tables/dictionary/table.sql b/packages/faker/verify/schemas/faker/tables/dictionary/table.sql index 43160acda..0faee4dbe 100644 --- a/packages/faker/verify/schemas/faker/tables/dictionary/table.sql +++ b/packages/faker/verify/schemas/faker/tables/dictionary/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('faker.dictionary'); +SELECT assert_table('faker.dictionary'::regclass); ROLLBACK; diff --git a/packages/function-resolution/Makefile b/packages/function-resolution/Makefile index 9778d70dd..02c5fdf4d 100644 --- a/packages/function-resolution/Makefile +++ b/packages/function-resolution/Makefile @@ -1,5 +1,5 @@ EXTENSION = pgpm-function-resolution -DATA = sql/pgpm-function-resolution--0.36.0.sql +DATA = sql/pgpm-function-resolution--0.39.0.sql PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) diff --git a/packages/function-resolution/__tests__/capabilities.test.ts b/packages/function-resolution/__tests__/capabilities.test.ts new file mode 100644 index 000000000..547fdd4d3 --- /dev/null +++ b/packages/function-resolution/__tests__/capabilities.test.ts @@ -0,0 +1,580 @@ +import { getConnections, PgTestClient } from 'pgsql-test'; + +let pg: PgTestClient; +let teardown: () => Promise; + +// Deterministic fixture ids. +const PLATFORM_DB = '11111111-1111-1111-1111-111111111111'; +const TENANT_DB = '22222222-2222-2222-2222-222222222222'; +const OTHER_DB = '33333333-3333-3333-3333-333333333333'; +const ORG_ID = '44444444-4444-4444-4444-444444444444'; + +const ids: Record = {}; + +// Capability resolution: a tenant-agnostic declaration plus a tenant's own +// labelled rows (or an explicit binding) produce exactly one answer per +// capability, or the invocation fails loudly before any code runs. +describe('function-resolution capability resolution', () => { + beforeAll(async () => { + ({ pg, teardown } = await getConnections()); + + await pg.query( + `INSERT INTO metaschema_public.database (id, name, platform) + VALUES ($1, 'platform_db', true), ($2, 'tenant_db', false), ($3, 'other_db', false)`, + [PLATFORM_DB, TENANT_DB, OTHER_DB] + ); + await pg.query( + `UPDATE metaschema_public.database SET owner_id = $2 WHERE id = $1`, + [TENANT_DB, ORG_ID] + ); + + // --- Definitions + capability bindings (database scope) ----------------- + await pg.query(`CREATE SCHEMA cap_defs`); + await pg.query( + `CREATE TABLE cap_defs.function_definitions ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + database_id uuid, + task_identifier text NOT NULL, + access_channels text[] NOT NULL DEFAULT '{}', + required_buckets text[] NOT NULL DEFAULT '{}', + required_modules text[] NOT NULL DEFAULT '{}', + required_models text[] NOT NULL DEFAULT '{}', + integrations text[] NOT NULL DEFAULT '{}' + )` + ); + await pg.query( + `CREATE TABLE cap_defs.function_capability_bindings ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + database_id uuid, + function_id uuid, + graph_id uuid, + bucket_id uuid, + key text NOT NULL, + lifecycle text NOT NULL DEFAULT 'execution' + )` + ); + + // --- Typed catalog: functions + buckets + apis -------------------------- + await pg.query(`CREATE SCHEMA catalog_private`); + await pg.query( + `CREATE TABLE catalog_private.functions ( + id uuid PRIMARY KEY, + owner_scope text NOT NULL, + owner_key uuid, + is_visible boolean NOT NULL DEFAULT false, + database_id uuid NOT NULL, + task_identifier text NOT NULL + )` + ); + await pg.query( + `CREATE TABLE catalog_private.buckets ( + id uuid PRIMARY KEY, + owner_scope text NOT NULL, + owner_key uuid, + is_visible boolean NOT NULL DEFAULT false, + database_id uuid NOT NULL, + key text NOT NULL, + type text NOT NULL, + physical_name text, + tags text[] + )` + ); + await pg.query( + `CREATE TABLE catalog_private.apis ( + id uuid PRIMARY KEY, + owner_scope text NOT NULL, + owner_key uuid, + is_visible boolean NOT NULL DEFAULT false, + database_id uuid NOT NULL, + name text NOT NULL + )` + ); + // The bindings kind of the same plane. Its lifecycle values order a tie + // within one frame; they are not scopes. + await pg.query( + `CREATE TABLE catalog_private.bindings ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + owner_scope text NOT NULL, + owner_key uuid, + database_id uuid NOT NULL, + function_id uuid, + graph_id uuid, + bucket_id uuid, + key text NOT NULL, + lifecycle text NOT NULL DEFAULT 'execution' + )` + ); + + // --- Api surface source plane (what module selectors resolve through) --- + await pg.query(`CREATE SCHEMA api_src`); + await pg.query( + `CREATE TABLE api_src.apis ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + database_id uuid, + name text NOT NULL + )` + ); + await pg.query( + `CREATE TABLE api_src.api_schemas ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + database_id uuid, + api_id uuid NOT NULL, + schema_id uuid NOT NULL + )` + ); + + // A payload table reference target. + await pg.query(`CREATE SCHEMA docs`); + await pg.query(`CREATE TABLE docs.documents (id uuid PRIMARY KEY)`); + + // --- Metaschema wiring -------------------------------------------------- + const schemaIds = new Map(); + const reg = async (schemaName: string, tableName: string, database = TENANT_DB) => { + if (!schemaIds.has(schemaName)) { + const schema = await pg.one( + `INSERT INTO metaschema_public.schema (database_id, name, schema_name) + VALUES ($1, $2, $2) RETURNING id`, + [database, schemaName] + ); + schemaIds.set(schemaName, schema.id); + } + const schemaId = schemaIds.get(schemaName)!; + const table = await pg.one( + `INSERT INTO metaschema_public."table" (database_id, schema_id, name) + VALUES ($1, $2, $3) RETURNING id`, + [database, schemaId, tableName] + ); + return { schemaId, tableId: table.id }; + }; + + const defs = await reg('cap_defs', 'function_definitions'); + const bindings = await reg('cap_defs', 'function_capability_bindings'); + const catFunctions = await reg('catalog_private', 'functions'); + const catBuckets = await reg('catalog_private', 'buckets'); + const catApis = await reg('catalog_private', 'apis'); + const catBindings = await reg('catalog_private', 'bindings'); + const srcApis = await reg('api_src', 'apis'); + const srcApiSchemas = await reg('api_src', 'api_schemas'); + const docs = await reg('docs', 'documents'); + + ids.docsTable = docs.tableId; + ids.notifSchema = docs.schemaId; + + await pg.query( + `INSERT INTO metaschema_modules_public.function_module + (database_id, scope, entity_field, schema_id, private_schema_id, + definitions_table_id, bindings_table_id, capability_bindings_table_id) + VALUES ($1, 'database', 'database_id', $2, $2, $3, $3, $4)`, + [TENANT_DB, defs.schemaId, defs.tableId, bindings.tableId] + ); + await pg.query( + `INSERT INTO metaschema_modules_public.catalog_module + (database_id, schema_id, functions_table_id, + domains_table_id, apis_table_id, sites_table_id, namespaces_table_id, + resources_table_id, resource_definitions_table_id, + resource_installations_table_id, apps_table_id, buckets_table_id, + sites_web_config_table_id, sites_error_pages_table_id, + sites_app_links_table_id, sites_deep_links_table_id, + bindings_table_id, scope) + VALUES ($1, $2, $3, $3, $4, $3, $3, $3, $3, $3, $3, $5, $3, $3, $3, $3, $6, 'database')`, + [ + TENANT_DB, + catFunctions.schemaId, + catFunctions.tableId, + catApis.tableId, + catBuckets.tableId, + catBindings.tableId, + ] + ); + await pg.query( + `INSERT INTO metaschema_modules_public.api_surface_module + (database_id, scope, schema_id, apis_table_id, api_schemas_table_id, + api_settings_table_id, cors_settings_table_id) + VALUES ($1, 'database', $2, $3, $4, $4, $4)`, + [TENANT_DB, srcApis.schemaId, srcApis.tableId, srcApiSchemas.tableId] + ); + + // --- Buckets: tenant-owned, tagged -------------------------------------- + const bucket = async ( + key: string, + type: string, + tags: string[], + database = TENANT_DB, + isVisible = false + ) => { + const row = await pg.one( + `INSERT INTO catalog_private.buckets (id, owner_scope, owner_key, is_visible, database_id, key, type, physical_name, tags) + VALUES (gen_random_uuid(), 'database', $1, $2, $1, $3, $4, $5, $6) RETURNING id`, + [database, isVisible, key, type, `phys-${key}`, tags] + ); + return row.id; + }; + + ids.exports = await bucket('exports', 'private', ['exports']); + ids.publicVariants = await bucket('variants-public', 'public', ['variants']); + ids.privateVariants = await bucket('variants-private', 'private', ['variants']); + ids.foreign = await bucket('foreign', 'private', ['exports'], OTHER_DB); + + // --- Apis: catalog rows + source rows + a module attachment ------------- + const adminApi = await pg.one( + `INSERT INTO api_src.apis (database_id, name) VALUES ($1, 'admin') RETURNING id`, + [TENANT_DB] + ); + ids.adminApi = adminApi.id; + await pg.query( + `INSERT INTO catalog_private.apis (id, owner_scope, owner_key, is_visible, database_id, name) + VALUES ($1, 'database', $2, false, $2, 'admin')`, + [ids.adminApi, TENANT_DB] + ); + // notifications_module carries an api_name and a schema_id (and no scope + // column), so it stands in for any module whose schemas are attached to a + // surface — and proves the selector does not require a scoped registration. + const notifRefs = await pg.any( + `SELECT column_name + FROM information_schema.columns + WHERE table_schema = 'metaschema_modules_public' + AND table_name = 'notifications_module' + AND (column_name LIKE '%_table_id' OR column_name LIKE '%schema_id')` + ); + const notifColumns = ['database_id', 'api_name'].concat( + notifRefs.map((c: any) => c.column_name) + ); + await pg.query( + `INSERT INTO metaschema_modules_public.notifications_module (${notifColumns.join(', ')}) + VALUES (${notifColumns.map((_, i) => `$${i + 1}`).join(', ')})`, + [TENANT_DB, 'admin'].concat( + notifRefs.map((c: any) => + c.column_name.endsWith('_table_id') ? ids.docsTable : ids.notifSchema + ) + ) + ); + await pg.query( + `INSERT INTO api_src.api_schemas (database_id, api_id, schema_id) + VALUES ($1, $2, $3)`, + [TENANT_DB, ids.adminApi, ids.notifSchema] + ); + + // --- Definitions -------------------------------------------------------- + const exporter = await pg.one( + `INSERT INTO cap_defs.function_definitions + (database_id, task_identifier, access_channels, required_buckets, required_modules, required_models) + VALUES ($1, 'report:export', ARRAY['api'], ARRAY['exports'], ARRAY['notifications_module'], ARRAY['gpt-4o']) + RETURNING id`, + [TENANT_DB] + ); + ids.exporter = exporter.id; + const ambiguous = await pg.one( + `INSERT INTO cap_defs.function_definitions + (database_id, task_identifier, required_buckets) + VALUES ($1, 'image:variants', ARRAY['variants']) RETURNING id`, + [TENANT_DB] + ); + ids.ambiguous = ambiguous.id; + + for (const id of [ids.exporter, ids.ambiguous]) { + await pg.query( + `INSERT INTO catalog_private.functions (id, owner_scope, owner_key, is_visible, database_id, task_identifier) + SELECT d.id, 'database', d.database_id, false, d.database_id, d.task_identifier + FROM cap_defs.function_definitions d WHERE d.id = $1`, + [id] + ); + } + }); + + afterAll(async () => { + await teardown(); + }); + + it('resolve_bucket(): one tag match resolves to coordinates', async () => { + const [row] = await pg.any( + `SELECT bucket_id, bucket_key, bucket_type, physical_name, owner_database_id + FROM function_resolution.resolve_bucket($1, 'database', $1, ARRAY['exports'], NULL)`, + [TENANT_DB] + ); + expect(row).toEqual({ + bucket_id: ids.exports, + bucket_key: 'exports', + bucket_type: 'private', + physical_name: 'phys-exports', + owner_database_id: TENANT_DB, + }); + }); + + it('resolve_bucket(): type narrows an otherwise ambiguous tag', async () => { + const [priv] = await pg.any( + `SELECT bucket_id FROM function_resolution.resolve_bucket($1, 'database', $1, ARRAY['variants'], 'private')`, + [TENANT_DB] + ); + expect(priv.bucket_id).toBe(ids.privateVariants); + + const [pub] = await pg.any( + `SELECT bucket_id FROM function_resolution.resolve_bucket($1, 'database', $1, ARRAY['variants'], 'public')`, + [TENANT_DB] + ); + expect(pub.bucket_id).toBe(ids.publicVariants); + }); + + it('resolve_bucket(): zero and several matches both fail loudly', async () => { + await expect( + pg.any( + `SELECT * FROM function_resolution.resolve_bucket($1, 'database', $1, ARRAY['nope'], NULL)`, + [TENANT_DB] + ) + ).rejects.toThrow(/CAPABILITY_BUCKET_NOT_FOUND/); + + await expect( + pg.any( + `SELECT * FROM function_resolution.resolve_bucket($1, 'database', $1, ARRAY['variants'], NULL)`, + [TENANT_DB] + ) + ).rejects.toThrow(/CAPABILITY_BUCKET_AMBIGUOUS/); + + await expect( + pg.any( + `SELECT * FROM function_resolution.resolve_bucket($1, 'database', $1, ARRAY[]::text[], NULL)`, + [TENANT_DB] + ) + ).rejects.toThrow(/CAPABILITY_BUCKET_SELECTOR_EMPTY/); + }); + + it('resolve_bucket(): another tenant\'s bucket is not a candidate', async () => { + // The foreign bucket carries the exports tag too, and stays invisible. + const [row] = await pg.any( + `SELECT bucket_id FROM function_resolution.resolve_bucket($1, 'database', $1, ARRAY['exports'], NULL)`, + [TENANT_DB] + ); + expect(row.bucket_id).toBe(ids.exports); + expect(row.bucket_id).not.toBe(ids.foreign); + }); + + it('resolve_api(): by module name, and by api name as the escape hatch', async () => { + // A plain word that is not a module name is read as an api name. + const [byName] = await pg.any( + `SELECT api_id, api_name FROM function_resolution.resolve_api($1, 'database', $1, 'admin')`, + [TENANT_DB] + ); + expect(byName).toEqual({ api_id: ids.adminApi, api_name: 'admin' }); + + // A module selector resolves through the api_schemas attachment — so it + // survives a rename of the api the module named at registration time. + await pg.query(`UPDATE api_src.apis SET name = 'admin_renamed' WHERE id = $1`, [ids.adminApi]); + await pg.query(`UPDATE catalog_private.apis SET name = 'admin_renamed' WHERE id = $1`, [ids.adminApi]); + + const [byModule] = await pg.any( + `SELECT api_id, api_name FROM function_resolution.resolve_api($1, 'database', $1, 'notifications_module')`, + [TENANT_DB] + ); + expect(byModule).toEqual({ api_id: ids.adminApi, api_name: 'admin_renamed' }); + + // . names the surface outright, and follows the rename. + const [byModuleApi] = await pg.any( + `SELECT api_id FROM function_resolution.resolve_api($1, 'database', $1, 'notifications_module.admin_renamed')`, + [TENANT_DB] + ); + expect(byModuleApi.api_id).toBe(ids.adminApi); + + // ...while the bare api name now misses, which is the point of the pair. + await expect( + pg.any(`SELECT * FROM function_resolution.resolve_api($1, 'database', $1, 'admin')`, [ + TENANT_DB, + ]) + ).rejects.toThrow(/CAPABILITY_API_NOT_FOUND/); + + await pg.query(`UPDATE api_src.apis SET name = 'admin' WHERE id = $1`, [ids.adminApi]); + await pg.query(`UPDATE catalog_private.apis SET name = 'admin' WHERE id = $1`, [ids.adminApi]); + }); + + it('resolve_api(): a module selector naming an api the module has not attached fails', async () => { + await expect( + pg.any( + `SELECT * FROM function_resolution.resolve_api($1, 'database', $1, 'notifications_module.nope')`, + [TENANT_DB] + ) + ).rejects.toThrow(/CAPABILITY_API_NOT_FOUND/); + }); + + it('resolve_api(): unknown module, qualified api name, and empty selector fail loudly', async () => { + await expect( + pg.any(`SELECT * FROM function_resolution.resolve_api($1, 'database', $1, 'nope_module')`, [ + TENANT_DB, + ]) + ).rejects.toThrow(/CAPABILITY_API_MODULE_UNKNOWN/); + + // notifications_module has no scope column, so pinning a scope is a + // selector error rather than a silent miss. + await expect( + pg.any( + `SELECT * FROM function_resolution.resolve_api($1, 'database', $1, 'notifications_module@org')`, + [TENANT_DB] + ) + ).rejects.toThrow(/CAPABILITY_API_SELECTOR_INVALID/); + + // Suffixes belong to module selectors only. + await expect( + pg.any(`SELECT * FROM function_resolution.resolve_api($1, 'database', $1, 'admin@org')`, [ + TENANT_DB, + ]) + ).rejects.toThrow(/CAPABILITY_API_SELECTOR_INVALID/); + + await expect( + pg.any(`SELECT * FROM function_resolution.resolve_api($1, 'database', $1, '')`, [TENANT_DB]) + ).rejects.toThrow(/CAPABILITY_API_SELECTOR_EMPTY/); + }); + + it('resolve_payload_refs(): rewrites tagged refs anywhere in the payload', async () => { + const [{ resolved }] = await pg.any( + `SELECT function_resolution.resolve_payload_refs($1, 'database', $1, $2::jsonb) AS resolved`, + [ + TENANT_DB, + JSON.stringify({ + plain: 'left alone', + nested: { + source: { $ref: 'bucket', tags: ['variants'], type: 'private' }, + list: [{ $ref: 'table', schema: 'docs', name: 'documents' }], + }, + surface: { $ref: 'api', module: 'notifications_module' }, + }), + ] + ); + + expect(resolved.plain).toBe('left alone'); + expect(resolved.nested.source).toEqual({ + $ref: 'bucket', + bucket_id: ids.privateVariants, + key: 'variants-private', + type: 'private', + physical_name: 'phys-variants-private', + database_id: TENANT_DB, + }); + expect(resolved.nested.list[0]).toEqual({ + $ref: 'table', + schema: 'docs', + name: 'documents', + table_id: ids.docsTable, + }); + expect(resolved.surface.api_id).toBe(ids.adminApi); + }); + + it('resolve_payload_refs(): an already-resolved payload is unchanged', async () => { + const once = await pg.one( + `SELECT function_resolution.resolve_payload_refs($1, 'database', $1, $2::jsonb) AS resolved`, + [TENANT_DB, JSON.stringify({ b: { $ref: 'bucket', tags: ['exports'] } })] + ); + const twice = await pg.one( + `SELECT function_resolution.resolve_payload_refs($1, 'database', $1, $2::jsonb) AS resolved`, + [TENANT_DB, JSON.stringify(once.resolved)] + ); + expect(twice.resolved).toEqual(once.resolved); + }); + + it("resolve_payload_refs(): $ref: 'secret' is not a ref kind, and bad table refs raise", async () => { + // A resolved reference becomes part of the payload the handler receives, so + // there is deliberately no secret kind — `ctx.secrets` fetches at + // invocation time instead. This asserts the rejection, not a lookup. + await expect( + pg.any( + `SELECT function_resolution.resolve_payload_refs($1, 'database', $1, $2::jsonb)`, + [TENANT_DB, JSON.stringify({ x: { $ref: 'secret', name: 'MAILGUN_API_KEY' } })] + ) + ).rejects.toThrow(/CAPABILITY_REF_UNKNOWN/); + + await expect( + pg.any( + `SELECT function_resolution.resolve_payload_refs($1, 'database', $1, $2::jsonb)`, + [TENANT_DB, JSON.stringify({ x: { $ref: 'table', schema: 'docs' } })] + ) + ).rejects.toThrow(/CAPABILITY_TABLE_REF_INVALID/); + + await expect( + pg.any( + `SELECT function_resolution.resolve_payload_refs($1, 'database', $1, $2::jsonb)`, + [TENANT_DB, JSON.stringify({ x: { $ref: 'table', schema: 'docs', name: 'missing' } })] + ) + ).rejects.toThrow(/CAPABILITY_TABLE_NOT_FOUND/); + }); + + it('resolve_capabilities(): declarations become one resolved bundle', async () => { + const [{ bundle }] = await pg.any( + `SELECT function_resolution.resolve_capabilities($1, 'database', $1, $2, 'database', $1, $3::jsonb, 'api') AS bundle`, + [TENANT_DB, ids.exporter, JSON.stringify({ subject: 'monthly' })] + ); + + expect(bundle.buckets.exports).toEqual({ + bucket_id: ids.exports, + key: 'exports', + type: 'private', + physical_name: 'phys-exports', + database_id: TENANT_DB, + source: 'tags', + }); + expect(bundle.apis['notifications_module'].api_id).toBe(ids.adminApi); + expect(bundle.models).toEqual(['gpt-4o']); + expect(bundle.payload).toEqual({ subject: 'monthly' }); + }); + + it('resolve_capabilities(): an explicit binding overrides tag discovery', async () => { + await pg.query( + `INSERT INTO catalog_private.bindings + (owner_scope, owner_key, database_id, function_id, bucket_id, key, lifecycle) + VALUES ('database', $1, $1, $2, $3, 'variants', 'execution')`, + [TENANT_DB, ids.ambiguous, ids.publicVariants] + ); + + const [{ bundle }] = await pg.any( + `SELECT function_resolution.resolve_capabilities($1, 'database', $1, $2, 'database', $1) AS bundle`, + [TENANT_DB, ids.ambiguous] + ); + expect(bundle.buckets.variants.bucket_id).toBe(ids.publicVariants); + expect(bundle.buckets.variants.source).toBe('binding'); + }); + + it('resolve_capabilities(): a binding on an unreachable bucket raises', async () => { + await pg.query( + `UPDATE catalog_private.bindings SET bucket_id = $1 WHERE function_id = $2`, + [ids.foreign, ids.ambiguous] + ); + + await expect( + pg.any( + `SELECT function_resolution.resolve_capabilities($1, 'database', $1, $2, 'database', $1)`, + [TENANT_DB, ids.ambiguous] + ) + ).rejects.toThrow(/CAPABILITY_BINDING_UNREACHABLE/); + + await pg.query(`DELETE FROM catalog_private.bindings WHERE function_id = $1`, [ + ids.ambiguous, + ]); + }); + + it('resolve_capabilities(): an undeclared channel is refused', async () => { + await expect( + pg.any( + `SELECT function_resolution.resolve_capabilities($1, 'database', $1, $2, 'database', $1, '{}'::jsonb, 'cron')`, + [TENANT_DB, ids.exporter] + ) + ).rejects.toThrow(/CAPABILITY_CHANNEL_REFUSED/); + }); + + it('resolve_capabilities(): a missing definition raises', async () => { + await expect( + pg.any( + `SELECT function_resolution.resolve_capabilities($1, 'database', $1, gen_random_uuid(), 'database', $1)`, + [TENANT_DB] + ) + ).rejects.toThrow(/CAPABILITY_DEFINITION_NOT_FOUND/); + }); + + it('validate_capabilities(): passes when resolvable, raises when not', async () => { + await pg.query( + `SELECT function_resolution.validate_capabilities($1, 'database', $1, $2, 'database', $1)`, + [TENANT_DB, ids.exporter] + ); + + await expect( + pg.any( + `SELECT function_resolution.validate_capabilities($1, 'database', $1, $2, 'database', $1)`, + [TENANT_DB, ids.ambiguous] + ) + ).rejects.toThrow(/CAPABILITY_BUCKET_AMBIGUOUS/); + }); +}); diff --git a/packages/function-resolution/__tests__/catalog-fastpath.test.ts b/packages/function-resolution/__tests__/catalog-fastpath.test.ts index d321bb1c9..91b2de36a 100644 --- a/packages/function-resolution/__tests__/catalog-fastpath.test.ts +++ b/packages/function-resolution/__tests__/catalog-fastpath.test.ts @@ -13,9 +13,11 @@ const BARE_DB = '55555555-5555-5555-5555-555555555555'; const ids: Record = {}; -// Catalog resolution proof: resolve answers from ONE typed catalog table per -// frame database (partial-unique-index probes), ordered by app_scope.frames; -// a frame database hosting function modules without a catalog fails loud. +// Catalog resolution proof: resolve answers from the published catalog plane +// (partial-unique-index probes), ordered by app_scope.frames; a frame database +// hosting function modules without a catalog module fails loud. +// The catalog rows here are projected by stand-in sync triggers, so this suite +// also pins that the projection — not the source table — is what resolution reads. describe('function-resolution catalog fast-path', () => { beforeAll(async () => { ({ pg, teardown } = await getConnections()); @@ -60,9 +62,9 @@ describe('function-resolution catalog fast-path', () => { ); // --- Catalog projection: one typed functions table, both scopes --------- - await pg.query(`CREATE SCHEMA cat_defs`); + await pg.query(`CREATE SCHEMA catalog_private`); await pg.query( - `CREATE TABLE cat_defs.functions ( + `CREATE TABLE catalog_private.functions ( id uuid PRIMARY KEY, owner_scope text NOT NULL, owner_key uuid, @@ -72,20 +74,20 @@ describe('function-resolution catalog fast-path', () => { )` ); await pg.query( - `CREATE UNIQUE INDEX functions_owner_scope_owner_key_task_identifier_idx - ON cat_defs.functions (owner_scope, owner_key, task_identifier) + `CREATE UNIQUE INDEX functions_db_owner_scope_owner_key_task_idx + ON catalog_private.functions (database_id, owner_scope, owner_key, task_identifier) WHERE owner_key IS NOT NULL` ); await pg.query( - `CREATE UNIQUE INDEX functions_owner_scope_task_identifier_idx - ON cat_defs.functions (owner_scope, task_identifier) + `CREATE UNIQUE INDEX functions_db_owner_scope_task_idx + ON catalog_private.functions (database_id, owner_scope, task_identifier) WHERE owner_key IS NULL` ); // Same-txn sync stand-in for the generated catalog_register triggers. await pg.query( - `CREATE FUNCTION cat_defs.tg_app_sync() RETURNS trigger AS $$ + `CREATE FUNCTION catalog_private.tg_app_sync() RETURNS trigger AS $$ BEGIN - INSERT INTO cat_defs.functions (id, owner_scope, owner_key, is_visible, database_id, task_identifier) + INSERT INTO catalog_private.functions (id, owner_scope, owner_key, is_visible, database_id, task_identifier) VALUES (NEW.id, 'app', NULL, COALESCE(NEW.is_published, false), '${TENANT_DB}', NEW.task_identifier) ON CONFLICT (id) DO UPDATE SET owner_key = EXCLUDED.owner_key, @@ -97,12 +99,12 @@ describe('function-resolution catalog fast-path', () => { ); await pg.query( `CREATE TRIGGER catalog_sync AFTER INSERT OR UPDATE ON app_defs.app_function_definitions - FOR EACH ROW EXECUTE FUNCTION cat_defs.tg_app_sync()` + FOR EACH ROW EXECUTE FUNCTION catalog_private.tg_app_sync()` ); await pg.query( - `CREATE FUNCTION cat_defs.tg_db_sync() RETURNS trigger AS $$ + `CREATE FUNCTION catalog_private.tg_db_sync() RETURNS trigger AS $$ BEGIN - INSERT INTO cat_defs.functions (id, owner_scope, owner_key, is_visible, database_id, task_identifier) + INSERT INTO catalog_private.functions (id, owner_scope, owner_key, is_visible, database_id, task_identifier) VALUES (NEW.id, 'database', NEW.database_id, COALESCE(NEW.is_published, false), COALESCE(NEW.database_id, '${TENANT_DB}'), NEW.task_identifier) ON CONFLICT (id) DO UPDATE SET @@ -115,7 +117,7 @@ describe('function-resolution catalog fast-path', () => { ); await pg.query( `CREATE TRIGGER catalog_sync AFTER INSERT OR UPDATE ON db_defs.db_function_definitions - FOR EACH ROW EXECUTE FUNCTION cat_defs.tg_db_sync()` + FOR EACH ROW EXECUTE FUNCTION catalog_private.tg_db_sync()` ); // --- Definitions (sync triggers project them into the catalog) ---------- @@ -166,7 +168,7 @@ describe('function-resolution catalog fast-path', () => { ); const catSchema = await pg.one( `INSERT INTO metaschema_public.schema (database_id, name, schema_name) - VALUES ($1, 'cat_defs', 'cat_defs') RETURNING id`, + VALUES ($1, 'catalog_private', 'catalog_private') RETURNING id`, [TENANT_DB] ); const catTable = await pg.one( @@ -197,8 +199,9 @@ describe('function-resolution catalog fast-path', () => { resources_table_id, resource_definitions_table_id, resource_installations_table_id, apps_table_id, buckets_table_id, sites_web_config_table_id, sites_error_pages_table_id, - sites_app_links_table_id, sites_deep_links_table_id, scope) - VALUES ($1, $2, $3, $3, $3, $3, $3, $3, $3, $3, $3, $3, $3, $3, $3, $3, 'app')`, + sites_app_links_table_id, sites_deep_links_table_id, + bindings_table_id, scope) + VALUES ($1, $2, $3, $3, $3, $3, $3, $3, $3, $3, $3, $3, $3, $3, $3, $3, $3, 'app')`, [TENANT_DB, catSchema.id, catTable.id] ); @@ -234,22 +237,6 @@ describe('function-resolution catalog fast-path', () => { }); - it('catalog_location(): resolves the functions catalog table', async () => { - const [row] = await pg.any( - `SELECT schema_name, table_name FROM function_resolution.catalog_location($1)`, - [TENANT_DB] - ); - expect(row).toEqual({ schema_name: 'cat_defs', table_name: 'functions' }); - }); - - it('catalog_location(): no catalog -> no rows', async () => { - const rows = await pg.any( - `SELECT * FROM function_resolution.catalog_location($1)`, - [BARE_DB] - ); - expect(rows).toEqual([]); - }); - it('resolve(): app-scope hit with owner database', async () => { const [row] = await pg.any( `SELECT function_definition_id, resolved_scope, owner_database_id @@ -274,7 +261,7 @@ describe('function-resolution catalog fast-path', () => { it('resolve(): unpublished (is_visible false) rows still resolve', async () => { const [{ is_visible }] = await pg.any( - `SELECT is_visible FROM cat_defs.functions WHERE id = $1`, + `SELECT is_visible FROM catalog_private.functions WHERE id = $1`, [ids.appDef] ); expect(is_visible).toBe(false); @@ -307,7 +294,7 @@ describe('function-resolution catalog fast-path', () => { }); it('resolve(): scope-default row answers when no exact scope-key row exists', async () => { - await pg.query(`DELETE FROM cat_defs.functions WHERE id = $1`, [ids.dbExact]); + await pg.query(`DELETE FROM catalog_private.functions WHERE id = $1`, [ids.dbExact]); const [row] = await pg.any( `SELECT function_definition_id, resolved_scope FROM function_resolution.resolve($1, 'database', NULL, 'report:run', true)`, @@ -315,7 +302,7 @@ describe('function-resolution catalog fast-path', () => { ); expect(row).toEqual({ function_definition_id: ids.dbDefault, resolved_scope: 'database' }); await pg.query( - `INSERT INTO cat_defs.functions (id, owner_scope, owner_key, is_visible, database_id, task_identifier) + `INSERT INTO catalog_private.functions (id, owner_scope, owner_key, is_visible, database_id, task_identifier) VALUES ($1, 'database', $2, false, $2, 'report:run')`, [ids.dbExact, TENANT_DB] ); @@ -323,7 +310,7 @@ describe('function-resolution catalog fast-path', () => { it('resolve(): the catalog is authoritative — a removed catalog row no longer resolves', async () => { await pg.query(`ALTER TABLE app_defs.app_function_definitions DISABLE TRIGGER catalog_sync`); - await pg.query(`DELETE FROM cat_defs.functions WHERE id = $1`, [ids.appDef]); + await pg.query(`DELETE FROM catalog_private.functions WHERE id = $1`, [ids.appDef]); await expect( pg.any( @@ -334,7 +321,7 @@ describe('function-resolution catalog fast-path', () => { // Restore. await pg.query( - `INSERT INTO cat_defs.functions (id, owner_scope, owner_key, is_visible, database_id, task_identifier) + `INSERT INTO catalog_private.functions (id, owner_scope, owner_key, is_visible, database_id, task_identifier) VALUES ($1, 'app', NULL, false, $2, 'email:send')`, [ids.appDef, TENANT_DB] ); @@ -365,7 +352,7 @@ describe('function-resolution catalog fast-path', () => { it('resolve_invocation(): a removed catalog row invalidates the pair', async () => { await pg.query(`ALTER TABLE app_defs.app_function_definitions DISABLE TRIGGER catalog_sync`); - await pg.query(`DELETE FROM cat_defs.functions WHERE id = $1`, [ids.appDef]); + await pg.query(`DELETE FROM catalog_private.functions WHERE id = $1`, [ids.appDef]); await expect( pg.any( @@ -375,7 +362,7 @@ describe('function-resolution catalog fast-path', () => { ).rejects.toThrow(/FUNCTION_DEFINITION_INVALID_PAIR/); await pg.query( - `INSERT INTO cat_defs.functions (id, owner_scope, owner_key, is_visible, database_id, task_identifier) + `INSERT INTO catalog_private.functions (id, owner_scope, owner_key, is_visible, database_id, task_identifier) VALUES ($1, 'app', NULL, false, $2, 'email:send')`, [ids.appDef, TENANT_DB] ); @@ -402,34 +389,39 @@ describe('function-resolution catalog fast-path', () => { }); it('EXPLAIN: catalog probe uses the partial unique indexes', async () => { - const [{ schema_name, table_name }] = await pg.any( - `SELECT schema_name, table_name FROM function_resolution.catalog_location($1)`, - [TENANT_DB] - ); const rows = await pg.any( `EXPLAIN (FORMAT text) SELECT hit.id - FROM unnest($1::text[], $2::uuid[], $3::bigint[]) AS cand(owner_scope, owner_key, ord) + FROM unnest($1::text[], $2::uuid[], $3::uuid[], $4::bigint[]) + AS cand(owner_scope, owner_key, lookup_database_id, ord) CROSS JOIN LATERAL ( - SELECT c.id FROM ${schema_name}.${table_name} c - WHERE c.task_identifier = $4 + SELECT c.id FROM catalog_private.functions c + WHERE c.task_identifier = $5 AND c.owner_scope = cand.owner_scope AND c.owner_key = cand.owner_key AND cand.owner_key IS NOT NULL + AND c.database_id = cand.owner_key UNION ALL - SELECT c.id FROM ${schema_name}.${table_name} c - WHERE c.task_identifier = $4 + SELECT c.id FROM catalog_private.functions c + WHERE c.task_identifier = $5 AND c.owner_scope = cand.owner_scope AND c.owner_key IS NULL AND cand.owner_key IS NULL + AND c.database_id = cand.lookup_database_id ) hit ORDER BY cand.ord LIMIT 1`, - [['database', 'database', 'app'], [TENANT_DB, null, null], [1, 2, 3], 'report:run'] + [ + ['database', 'database', 'app'], + [TENANT_DB, null, null], + [TENANT_DB, TENANT_DB, TENANT_DB], + [1, 2, 3], + 'report:run', + ] ); const plan = rows.map((r: any) => r['QUERY PLAN']).join('\n'); - expect(plan).toMatch(/functions_owner_scope_owner_key_task_identifier_idx/); - expect(plan).toMatch(/functions_owner_scope_task_identifier_idx/); - expect(plan).not.toMatch(/Seq Scan on cat_defs\.functions/); + expect(plan).toMatch(/functions_db_owner_scope_owner_key_task_idx/); + expect(plan).toMatch(/functions_db_owner_scope_task_idx/); + expect(plan).not.toMatch(/Seq Scan on catalog_private\.functions/); }); }); diff --git a/packages/function-resolution/__tests__/portability.test.ts b/packages/function-resolution/__tests__/portability.test.ts index 23a274ad7..1e7411491 100644 --- a/packages/function-resolution/__tests__/portability.test.ts +++ b/packages/function-resolution/__tests__/portability.test.ts @@ -59,7 +59,7 @@ describe('function-resolution portability', () => { `SELECT n.nspname, p.proname FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace WHERE (n.nspname = 'app_scope' AND p.proname IN ('frames','platform_database_id','membership_parent','dyn_lookup_uuid')) - OR (n.nspname = 'function_resolution' AND p.proname IN ('resolve','routing','definitions_location','catalog_location','resolve_invocation','enqueue')) + OR (n.nspname = 'function_resolution' AND p.proname IN ('resolve','routing','definitions_location','resolve_invocation','enqueue')) ORDER BY n.nspname, p.proname` ); const names = rows.map((r: any) => `${r.nspname}.${r.proname}`); @@ -68,7 +68,6 @@ describe('function-resolution portability', () => { 'app_scope.frames', 'app_scope.membership_parent', 'app_scope.platform_database_id', - 'function_resolution.catalog_location', 'function_resolution.definitions_location', 'function_resolution.enqueue', 'function_resolution.resolve', diff --git a/packages/function-resolution/__tests__/published-capability-catalog.test.ts b/packages/function-resolution/__tests__/published-capability-catalog.test.ts new file mode 100644 index 000000000..8b114a3b2 --- /dev/null +++ b/packages/function-resolution/__tests__/published-capability-catalog.test.ts @@ -0,0 +1,423 @@ +import { getConnections, PgTestClient } from 'pgsql-test'; + +let pg: PgTestClient; +let teardown: () => Promise; + +// Deterministic fixture ids. +const PLATFORM_DB = '11111111-1111-1111-1111-111111111111'; +const DB_A = '22222222-2222-2222-2222-222222222222'; +const DB_B = '33333333-3333-3333-3333-333333333333'; +// A database still registering into its own hash-named catalog, as databases +// provisioned before the published module do. +const LEGACY_DB = '44444444-4444-4444-4444-444444444444'; + +const ids: Record = {}; + +// The published capability planes: ONE catalog_private.buckets and ONE +// catalog_private.apis holding every database's rows, separated only by the +// database_id the catalog-sync triggers stamp. The hazard this suite pins is a +// probe answered with another tenant's row — the shared plane makes that a +// single missing predicate away, and both resolvers must be tenant-exact while +// the legacy database keeps reading its own plane. +describe('capability resolution against the published catalog planes', () => { + beforeAll(async () => { + ({ pg, teardown } = await getConnections()); + + await pg.query( + `INSERT INTO metaschema_public.database (id, name, platform) + VALUES ($1, 'platform_db', true), ($2, 'db_a', false), + ($3, 'db_b', false), ($4, 'legacy_db', false)`, + [PLATFORM_DB, DB_A, DB_B, LEGACY_DB] + ); + + // --- The published planes ------------------------------------------------ + await pg.query(`CREATE SCHEMA catalog_private`); + await pg.query( + `CREATE TABLE catalog_private.buckets ( + id uuid PRIMARY KEY, + owner_scope text NOT NULL, + owner_key uuid, + is_visible boolean NOT NULL DEFAULT false, + database_id uuid NOT NULL, + key text NOT NULL, + type text NOT NULL, + physical_name text, + tags text[] + )` + ); + // The database-qualified claims the published catalog carries: two + // databases may each own a bucket with the same (scope, key, bucket key). + await pg.query( + `CREATE UNIQUE INDEX buckets_db_owner_key_idx + ON catalog_private.buckets (database_id, owner_scope, owner_key, key) + WHERE owner_key IS NOT NULL` + ); + await pg.query( + `CREATE UNIQUE INDEX buckets_db_owner_scope_key_idx + ON catalog_private.buckets (database_id, owner_scope, key) + WHERE owner_key IS NULL` + ); + await pg.query( + `CREATE TABLE catalog_private.apis ( + id uuid PRIMARY KEY, + owner_scope text NOT NULL, + owner_key uuid, + is_visible boolean NOT NULL DEFAULT false, + database_id uuid NOT NULL, + name text NOT NULL + )` + ); + await pg.query( + `CREATE UNIQUE INDEX apis_db_owner_key_name_idx + ON catalog_private.apis (database_id, owner_scope, owner_key, name) + WHERE owner_key IS NOT NULL` + ); + await pg.query( + `CREATE UNIQUE INDEX apis_db_owner_scope_name_idx + ON catalog_private.apis (database_id, owner_scope, name) + WHERE owner_key IS NULL` + ); + + await pg.query( + `CREATE TABLE catalog_private.bindings ( + id uuid PRIMARY KEY, + owner_scope text NOT NULL, + owner_key uuid, + is_visible boolean NOT NULL DEFAULT false, + database_id uuid NOT NULL, + function_id uuid, + graph_id uuid, + bucket_id uuid, + lifecycle text NOT NULL, + key text NOT NULL + )` + ); + await pg.query( + `CREATE INDEX bindings_db_owner_fn_key_idx + ON catalog_private.bindings (database_id, owner_scope, owner_key, function_id, key)` + ); + + // --- A legacy, hash-named plane of the same shape ------------------------ + await pg.query(`CREATE SCHEMA cap_legacy_catalog_private`); + await pg.query( + `CREATE TABLE cap_legacy_catalog_private.buckets ( + id uuid PRIMARY KEY, + owner_scope text NOT NULL, + owner_key uuid, + is_visible boolean NOT NULL DEFAULT false, + database_id uuid NOT NULL, + key text NOT NULL, + type text NOT NULL, + physical_name text, + tags text[] + )` + ); + await pg.query( + `CREATE TABLE cap_legacy_catalog_private.apis ( + id uuid PRIMARY KEY, + owner_scope text NOT NULL, + owner_key uuid, + is_visible boolean NOT NULL DEFAULT false, + database_id uuid NOT NULL, + name text NOT NULL + )` + ); + + // --- Rows: the same identity in two databases --------------------------- + ids.bucketA = 'aaaaaaaa-0000-0000-0000-000000000001'; + ids.bucketB = 'bbbbbbbb-0000-0000-0000-000000000001'; + ids.bucketAVisible = 'aaaaaaaa-0000-0000-0000-000000000002'; + ids.bucketLegacy = 'cccccccc-0000-0000-0000-000000000001'; + ids.apiA = 'aaaaaaaa-0000-0000-0000-000000000011'; + ids.apiB = 'bbbbbbbb-0000-0000-0000-000000000011'; + ids.apiLegacy = 'cccccccc-0000-0000-0000-000000000011'; + // One function definition id, bound by BOTH databases at the same (scope, + // key) coordinates: the pair the shared bindings plane must keep apart. + ids.functionDef = 'dddddddd-0000-0000-0000-000000000001'; + ids.bindingA = 'aaaaaaaa-0000-0000-0000-000000000021'; + ids.bindingB = 'bbbbbbbb-0000-0000-0000-000000000021'; + + await pg.query( + `INSERT INTO catalog_private.buckets + (id, owner_scope, owner_key, is_visible, database_id, key, type, physical_name, tags) + VALUES ($1, 'database', $2, false, $2, 'exports', 'private', 'phys-a-exports', ARRAY['exports']), + ($3, 'database', $4, false, $4, 'exports', 'private', 'phys-b-exports', ARRAY['exports']), + ($5, 'database', $2, true, $2, 'shared', 'public', 'phys-a-shared', ARRAY['shared'])`, + [ids.bucketA, DB_A, ids.bucketB, DB_B, ids.bucketAVisible] + ); + await pg.query( + `INSERT INTO catalog_private.apis + (id, owner_scope, owner_key, is_visible, database_id, name) + VALUES ($1, 'database', $2, false, $2, 'admin'), + ($3, 'database', $4, false, $4, 'admin')`, + [ids.apiA, DB_A, ids.apiB, DB_B] + ); + await pg.query( + `INSERT INTO cap_legacy_catalog_private.buckets + (id, owner_scope, owner_key, is_visible, database_id, key, type, physical_name, tags) + VALUES ($1, 'database', $2, false, $2, 'exports', 'private', 'phys-legacy-exports', ARRAY['exports'])`, + [ids.bucketLegacy, LEGACY_DB] + ); + await pg.query( + `INSERT INTO cap_legacy_catalog_private.apis + (id, owner_scope, owner_key, is_visible, database_id, name) + VALUES ($1, 'database', $2, false, $2, 'admin')`, + [ids.apiLegacy, LEGACY_DB] + ); + + await pg.query( + `INSERT INTO catalog_private.bindings + (id, owner_scope, owner_key, database_id, function_id, bucket_id, lifecycle, key) + VALUES ($1, 'database', $2, $2, $5, $6, 'deployment', 'reports'), + ($3, 'database', $4, $4, $5, $7, 'deployment', 'reports')`, + [ids.bindingA, DB_A, ids.bindingB, DB_B, ids.functionDef, ids.bucketA, ids.bucketB] + ); + + // --- Metaschema wiring: which plane each database registers into -------- + // The published planes are ONE physical schema, so they have ONE metaschema + // schema row (schema_name is globally unique) and one table row per kind; + // every database that deploys the published module points its + // catalog_module at them. That is exactly why the rows carry database_id. + const publishedSchema = await pg.one( + `INSERT INTO metaschema_public.schema (database_id, name, schema_name) + VALUES ($1, 'catalog_private', 'catalog_private') RETURNING id`, + [PLATFORM_DB] + ); + const publishedBuckets = await pg.one( + `INSERT INTO metaschema_public."table" (database_id, schema_id, name) + VALUES ($1, $2, 'buckets') RETURNING id`, + [PLATFORM_DB, publishedSchema.id] + ); + const publishedApis = await pg.one( + `INSERT INTO metaschema_public."table" (database_id, schema_id, name) + VALUES ($1, $2, 'apis') RETURNING id`, + [PLATFORM_DB, publishedSchema.id] + ); + const publishedBindings = await pg.one( + `INSERT INTO metaschema_public."table" (database_id, schema_id, name) + VALUES ($1, $2, 'bindings') RETURNING id`, + [PLATFORM_DB, publishedSchema.id] + ); + + for (const [dbId, schemaName, label] of [ + [DB_A, 'catalog_private', 'a'], + [DB_B, 'catalog_private', 'b'], + [LEGACY_DB, 'cap_legacy_catalog_private', 'legacy'], + ] as const) { + let bucketsTableId = publishedBuckets.id; + let apisTableId = publishedApis.id; + let bindingsTableId = publishedBindings.id; + let schemaId = publishedSchema.id; + + if (schemaName !== 'catalog_private') { + const schema = await pg.one( + `INSERT INTO metaschema_public.schema (database_id, name, schema_name) + VALUES ($1, $2, $2) RETURNING id`, + [dbId, schemaName] + ); + const buckets = await pg.one( + `INSERT INTO metaschema_public."table" (database_id, schema_id, name) + VALUES ($1, $2, 'buckets') RETURNING id`, + [dbId, schema.id] + ); + const apis = await pg.one( + `INSERT INTO metaschema_public."table" (database_id, schema_id, name) + VALUES ($1, $2, 'apis') RETURNING id`, + [dbId, schema.id] + ); + // The legacy database's catalog has no bindings table at all, as every + // database provisioned before the bindings kind does: its bindings stay + // in the scoped source table the transition branch reads. + schemaId = schema.id; + bucketsTableId = buckets.id; + apisTableId = apis.id; + bindingsTableId = buckets.id; + } + + await pg.query( + `INSERT INTO metaschema_modules_public.catalog_module + (database_id, schema_id, functions_table_id, + domains_table_id, apis_table_id, sites_table_id, namespaces_table_id, + resources_table_id, resource_definitions_table_id, + resource_installations_table_id, apps_table_id, buckets_table_id, + bindings_table_id, sites_web_config_table_id, + sites_error_pages_table_id, + sites_app_links_table_id, sites_deep_links_table_id, scope) + VALUES ($1, $2, $3, $3, $4, $3, $3, $3, $3, $3, $3, $5, $6, $3, $3, $3, $3, 'database')`, + [dbId, schemaId, bucketsTableId, apisTableId, bucketsTableId, bindingsTableId] + ); + // Label kept for readability of the fixture rows above. + void label; + } + }); + + afterAll(async () => { + await teardown(); + }); + + it('resolve_bucket(): each database is answered with its own row', async () => { + const [a] = await pg.any( + `SELECT bucket_id, physical_name, owner_database_id + FROM function_resolution.resolve_bucket($1, 'database', $1, ARRAY['exports'], NULL)`, + [DB_A] + ); + expect(a).toEqual({ + bucket_id: ids.bucketA, + physical_name: 'phys-a-exports', + owner_database_id: DB_A, + }); + + const [b] = await pg.any( + `SELECT bucket_id, physical_name, owner_database_id + FROM function_resolution.resolve_bucket($1, 'database', $1, ARRAY['exports'], NULL)`, + [DB_B] + ); + expect(b).toEqual({ + bucket_id: ids.bucketB, + physical_name: 'phys-b-exports', + owner_database_id: DB_B, + }); + }); + + it('resolve_bucket(): a plane that is not the published one is read by nothing', async () => { + // LEGACY_DB's rows sit in its own hash-named plane. The fallback that used + // to read it is gone, so its own bucket is simply not found. + await expect( + pg.any( + `SELECT * FROM function_resolution.resolve_bucket($1, 'database', $1, ARRAY['exports'], NULL)`, + [LEGACY_DB] + ) + ).rejects.toThrow(/CAPABILITY_BUCKET_NOT_FOUND/); + }); + + it('resolve_bucket(): a tag present only in another database is not found', async () => { + await pg.query( + `INSERT INTO catalog_private.buckets + (id, owner_scope, owner_key, is_visible, database_id, key, type, physical_name, tags) + VALUES ($1, 'database', $2, false, $2, 'b-only', 'private', 'phys-b-only', ARRAY['b-only'])`, + ['bbbbbbbb-0000-0000-0000-000000000002', DB_B] + ); + await expect( + pg.any( + `SELECT * FROM function_resolution.resolve_bucket($1, 'database', $1, ARRAY['b-only'], NULL)`, + [DB_A] + ) + ).rejects.toThrow(/CAPABILITY_BUCKET_NOT_FOUND/); + }); + + it('bucket_catalog_row(): a binding cannot reach another database bucket', async () => { + const [own] = await pg.any( + `SELECT bucket_key, physical_name, owner_database_id + FROM function_resolution.bucket_catalog_row($1, 'database', $1, $2)`, + [DB_A, ids.bucketA] + ); + expect(own).toEqual({ + bucket_key: 'exports', + physical_name: 'phys-a-exports', + owner_database_id: DB_A, + }); + + // DB_B's bucket lives in the same relation, is not visible, and must not + // be reachable from DB_A even though its id is known. + const foreign = await pg.any( + `SELECT * FROM function_resolution.bucket_catalog_row($1, 'database', $1, $2)`, + [DB_A, ids.bucketB] + ); + expect(foreign).toEqual([]); + }); + + it('bucket_catalog_row(): a visible bucket of another database is unreachable without a shared frame', async () => { + // is_visible relaxes the ownership check only for a frame in the caller's + // own chain; DB_B's chain contains no frame owning DB_A's row. + const rows = await pg.any( + `SELECT * FROM function_resolution.bucket_catalog_row($1, 'database', $1, $2)`, + [DB_B, ids.bucketAVisible] + ); + expect(rows).toEqual([]); + }); + + it('resolve_api(): the name path answers each database with its own surface', async () => { + const [a] = await pg.any( + `SELECT api_id, api_name, owner_database_id + FROM function_resolution.resolve_api($1, 'database', $1, 'admin')`, + [DB_A] + ); + expect(a).toEqual({ api_id: ids.apiA, api_name: 'admin', owner_database_id: DB_A }); + + const [b] = await pg.any( + `SELECT api_id, owner_database_id + FROM function_resolution.resolve_api($1, 'database', $1, 'admin')`, + [DB_B] + ); + expect(b).toEqual({ api_id: ids.apiB, owner_database_id: DB_B }); + + // LEGACY_DB carries the same name in its own hash-named plane, which is no + // longer read at all. + await expect( + pg.any(`SELECT * FROM function_resolution.resolve_api($1, 'database', $1, 'admin')`, [ + LEGACY_DB, + ]) + ).rejects.toThrow(/CAPABILITY_API_NOT_FOUND/); + }); + + it('resolve_api(): an api present only in another database is not found', async () => { + await pg.query( + `INSERT INTO catalog_private.apis + (id, owner_scope, owner_key, is_visible, database_id, name) + VALUES ($1, 'database', $2, false, $2, 'b-only-api')`, + ['bbbbbbbb-0000-0000-0000-000000000012', DB_B] + ); + await expect( + pg.any(`SELECT * FROM function_resolution.resolve_api($1, 'database', $1, 'b-only-api')`, [ + DB_A, + ]) + ).rejects.toThrow(/CAPABILITY_API_NOT_FOUND/); + }); + + it('api_catalog_row(): another database surface is unreachable by id', async () => { + const [own] = await pg.any( + `SELECT api_name, owner_database_id + FROM function_resolution.api_catalog_row($1, 'database', $1, $2)`, + [DB_A, ids.apiA] + ); + expect(own).toEqual({ api_name: 'admin', owner_database_id: DB_A }); + + const foreign = await pg.any( + `SELECT * FROM function_resolution.api_catalog_row($1, 'database', $1, $2)`, + [DB_A, ids.apiB] + ); + expect(foreign).toEqual([]); + }); + + it('bound_bucket_id(): each database is answered with its own binding', async () => { + const [a] = await pg.any( + `SELECT function_resolution.bound_bucket_id($1, 'database', $1, $2, 'reports') AS bucket_id`, + [DB_A, ids.functionDef] + ); + expect(a.bucket_id).toBe(ids.bucketA); + + const [b] = await pg.any( + `SELECT function_resolution.bound_bucket_id($1, 'database', $1, $2, 'reports') AS bucket_id`, + [DB_B, ids.functionDef] + ); + expect(b.bucket_id).toBe(ids.bucketB); + }); + + it('bound_bucket_id(): an unbound key is left to discovery', async () => { + const [none] = await pg.any( + `SELECT function_resolution.bound_bucket_id($1, 'database', $1, $2, 'exports') AS bucket_id`, + [DB_A, ids.functionDef] + ); + expect(none.bucket_id).toBeNull(); + }); + + it('the published planes are authoritative — removing the row stops resolution', async () => { + await pg.query(`DELETE FROM catalog_private.buckets WHERE id = $1`, [ids.bucketA]); + await expect( + pg.any( + `SELECT * FROM function_resolution.resolve_bucket($1, 'database', $1, ARRAY['exports'], NULL)`, + [DB_A] + ) + ).rejects.toThrow(/CAPABILITY_BUCKET_NOT_FOUND/); + }); +}); diff --git a/packages/function-resolution/__tests__/published-catalog.test.ts b/packages/function-resolution/__tests__/published-catalog.test.ts new file mode 100644 index 000000000..3ddad7c40 --- /dev/null +++ b/packages/function-resolution/__tests__/published-catalog.test.ts @@ -0,0 +1,362 @@ +import { getConnections, PgTestClient } from 'pgsql-test'; + +let pg: PgTestClient; +let teardown: () => Promise; + +// Deterministic fixture ids. +const PLATFORM_DB = '11111111-1111-1111-1111-111111111111'; +const DB_A = '22222222-2222-2222-2222-222222222222'; +const DB_B = '33333333-3333-3333-3333-333333333333'; +// A database registered into a plane that is NOT the published one. Nothing +// resolves for it any more: the transition fallback that read such a plane is +// gone, so it is here to pin that the published plane is the only one read. +const LEGACY_DB = '44444444-4444-4444-4444-444444444444'; + +const ids: Record = {}; + +// The published catalog plane: ONE relation (catalog_private.functions) holding +// every database's rows, separated by the database_id the catalog-sync +// triggers stamp. Resolution against it must be static AND tenant-exact — the +// hazard this suite pins is a probe answered with another database's row. +describe('function-resolution against the published catalog plane', () => { + beforeAll(async () => { + ({ pg, teardown } = await getConnections()); + + await pg.query( + `INSERT INTO metaschema_public.database (id, name, platform) + VALUES ($1, 'platform_db', true), ($2, 'db_a', false), + ($3, 'db_b', false), ($4, 'legacy_db', false)`, + [PLATFORM_DB, DB_A, DB_B, LEGACY_DB] + ); + + // --- The published plane ------------------------------------------------- + await pg.query(`CREATE SCHEMA catalog_private`); + await pg.query( + `CREATE TABLE catalog_private.functions ( + id uuid PRIMARY KEY, + owner_scope text NOT NULL, + owner_key uuid, + is_visible boolean NOT NULL DEFAULT false, + database_id uuid NOT NULL, + task_identifier text NOT NULL, + queue_name text, + priority integer, + max_attempts integer + )` + ); + // The unique keys the published catalog carries: database-qualified, so + // two databases may each own the same (scope, key, task). + await pg.query( + `CREATE UNIQUE INDEX functions_db_owner_scope_owner_key_task_idx + ON catalog_private.functions (database_id, owner_scope, owner_key, task_identifier) + WHERE owner_key IS NOT NULL` + ); + await pg.query( + `CREATE UNIQUE INDEX functions_db_owner_scope_task_idx + ON catalog_private.functions (database_id, owner_scope, task_identifier) + WHERE owner_key IS NULL` + ); + await pg.query( + `CREATE INDEX functions_database_id_idx ON catalog_private.functions (database_id)` + ); + + // --- A legacy, hash-named plane for the same shape ----------------------- + await pg.query(`CREATE SCHEMA fn_legacy_catalog_private`); + await pg.query( + `CREATE TABLE fn_legacy_catalog_private.functions ( + id uuid PRIMARY KEY, + owner_scope text NOT NULL, + owner_key uuid, + is_visible boolean NOT NULL DEFAULT false, + database_id uuid NOT NULL, + task_identifier text NOT NULL + )` + ); + await pg.query( + `CREATE UNIQUE INDEX legacy_functions_owner_scope_task_idx + ON fn_legacy_catalog_private.functions (owner_scope, task_identifier) + WHERE owner_key IS NULL` + ); + + // --- Rows ---------------------------------------------------------------- + // Same (owner_scope, owner_key, task_identifier) in two databases: only + // database_id tells them apart. + ids.appA = 'aaaaaaaa-0000-0000-0000-000000000001'; + ids.appB = 'bbbbbbbb-0000-0000-0000-000000000001'; + ids.dbScopedA = 'aaaaaaaa-0000-0000-0000-000000000002'; + ids.legacy = 'cccccccc-0000-0000-0000-000000000001'; + + await pg.query( + `INSERT INTO catalog_private.functions + (id, owner_scope, owner_key, is_visible, database_id, task_identifier, + queue_name, priority, max_attempts) + VALUES ($1, 'app', NULL, true, $2, 'email:send', 'email', 5, 3), + ($3, 'app', NULL, true, $4, 'email:send', 'default', 0, 25), + ($5, 'database', $2, true, $2, 'report:run', NULL, NULL, NULL)`, + [ids.appA, DB_A, ids.appB, DB_B, ids.dbScopedA] + ); + await pg.query( + `INSERT INTO fn_legacy_catalog_private.functions + (id, owner_scope, owner_key, is_visible, database_id, task_identifier) + VALUES ($1, 'app', NULL, true, $2, 'email:send')`, + [ids.legacy, LEGACY_DB] + ); + + // --- Metaschema wiring: which plane each database registers into --------- + // The published plane is ONE physical schema, so it has ONE metaschema + // schema/table row (schema_name is globally unique); every database that + // deploys the published module points its catalog_module at it. That is + // exactly why the rows carry database_id. + const publishedSchema = await pg.one( + `INSERT INTO metaschema_public.schema (database_id, name, schema_name) + VALUES ($1, 'catalog_private', 'catalog_private') RETURNING id`, + [PLATFORM_DB] + ); + const publishedTable = await pg.one( + `INSERT INTO metaschema_public."table" (database_id, schema_id, name) + VALUES ($1, $2, 'functions') RETURNING id`, + [PLATFORM_DB, publishedSchema.id] + ); + + for (const [dbId, schemaName, label] of [ + [DB_A, 'catalog_private', 'a'], + [DB_B, 'catalog_private', 'b'], + [LEGACY_DB, 'fn_legacy_catalog_private', 'legacy'], + ] as const) { + let schemaId = publishedSchema.id; + let tableId = publishedTable.id; + if (schemaName !== 'catalog_private') { + const schema = await pg.one( + `INSERT INTO metaschema_public.schema (database_id, name, schema_name) + VALUES ($1, $2, $2) RETURNING id`, + [dbId, schemaName] + ); + const table = await pg.one( + `INSERT INTO metaschema_public."table" (database_id, schema_id, name) + VALUES ($1, $2, 'functions') RETURNING id`, + [dbId, schema.id] + ); + schemaId = schema.id; + tableId = table.id; + } + await pg.query( + `INSERT INTO metaschema_modules_public.catalog_module + (database_id, schema_id, functions_table_id, + domains_table_id, apis_table_id, sites_table_id, namespaces_table_id, + resources_table_id, resource_definitions_table_id, + resource_installations_table_id, apps_table_id, buckets_table_id, + sites_web_config_table_id, sites_error_pages_table_id, + sites_app_links_table_id, sites_deep_links_table_id, + bindings_table_id, scope) + VALUES ($1, $2, $3, $3, $3, $3, $3, $3, $3, $3, $3, $3, $3, $3, $3, $3, $3, 'app')`, + [dbId, schemaId, tableId] + ); + + // Each database also hosts a function module, so a catalog miss would + // fail loud rather than being silently skipped. + const defsSchema = await pg.one( + `INSERT INTO metaschema_public.schema (database_id, name, schema_name) + VALUES ($1, $2, $2) RETURNING id`, + [dbId, `defs_${label}`] + ); + await pg.query(`CREATE SCHEMA defs_${label}`); + await pg.query( + `CREATE TABLE defs_${label}.app_function_definitions ( + id uuid PRIMARY KEY, + task_identifier text NOT NULL, + queue_name text, + priority integer, + max_attempts integer + )` + ); + const defsTable = await pg.one( + `INSERT INTO metaschema_public."table" (database_id, schema_id, name) + VALUES ($1, $2, 'app_function_definitions') RETURNING id`, + [dbId, defsSchema.id] + ); + await pg.query( + `INSERT INTO metaschema_modules_public.function_module + (database_id, scope, entity_field, schema_id, private_schema_id, + definitions_table_id, bindings_table_id) + VALUES ($1, 'app', NULL, $2, $2, $3, $3)`, + [dbId, defsSchema.id, defsTable.id] + ); + } + }); + + afterAll(async () => { + await teardown(); + }); + + it('resolve(): answers from the shared plane', async () => { + const [row] = await pg.any( + `SELECT function_definition_id, resolved_scope, owner_database_id + FROM function_resolution.resolve($1, 'app', NULL, 'email:send', true)`, + [DB_A] + ); + expect(row).toEqual({ + function_definition_id: ids.appA, + resolved_scope: 'app', + owner_database_id: DB_A, + }); + }); + + it('resolve(): a database never sees another database rows', async () => { + const [rowB] = await pg.any( + `SELECT function_definition_id, owner_database_id + FROM function_resolution.resolve($1, 'app', NULL, 'email:send', true)`, + [DB_B] + ); + expect(rowB).toEqual({ + function_definition_id: ids.appB, + owner_database_id: DB_B, + }); + + // LEGACY_DB has an identical row in its own hash-named plane, which is now + // read by nothing: only catalog_private answers, and it holds no row for + // that database. + await expect( + pg.any( + `SELECT * FROM function_resolution.resolve($1, 'app', NULL, 'email:send', true)`, + [LEGACY_DB] + ) + ).rejects.toThrow(/FUNCTION_DEFINITION_NOT_FOUND/); + }); + + it('resolve(): a task present only in another database is not found', async () => { + await pg.query( + `INSERT INTO catalog_private.functions + (id, owner_scope, owner_key, is_visible, database_id, task_identifier) + VALUES ($1, 'app', NULL, true, $2, 'b:only')`, + ['bbbbbbbb-0000-0000-0000-000000000002', DB_B] + ); + await expect( + pg.any( + `SELECT * FROM function_resolution.resolve($1, 'app', NULL, 'b:only', true)`, + [DB_A] + ) + ).rejects.toThrow(/FUNCTION_DEFINITION_NOT_FOUND/); + }); + + it('resolve(): database-scope rows key by their own database', async () => { + const [row] = await pg.any( + `SELECT function_definition_id, resolved_scope + FROM function_resolution.resolve($1, 'database', $1, 'report:run', true)`, + [DB_A] + ); + expect(row).toEqual({ + function_definition_id: ids.dbScopedA, + resolved_scope: 'database', + }); + + await expect( + pg.any( + `SELECT * FROM function_resolution.resolve($1, 'database', $1, 'report:run', true)`, + [DB_B] + ) + ).rejects.toThrow(/FUNCTION_DEFINITION_NOT_FOUND/); + }); + + it('resolve_invocation(): validates a pair against the owning database only', async () => { + const [row] = await pg.any( + `SELECT function_definition_id, definition_scope + FROM function_resolution.resolve_invocation($1, 'app', NULL, 'email:send', $2, 'app', NULL)`, + [DB_A, ids.appA] + ); + expect(row).toEqual({ function_definition_id: ids.appA, definition_scope: 'app' }); + + // DB_B's definition carries the same scope/key/task; validating it against + // DB_A must fail rather than pass on the shared relation. + await expect( + pg.any( + `SELECT * FROM function_resolution.resolve_invocation($1, 'app', NULL, 'email:send', $2, 'app', NULL)`, + [DB_A, ids.appB] + ) + ).rejects.toThrow(/FUNCTION_DEFINITION_INVALID_PAIR/); + }); + + it('routing(): reads queue behaviour from the same catalog row', async () => { + const [row] = await pg.any( + `SELECT queue_name, priority, max_attempts + FROM function_resolution.routing($1, $2)`, + [DB_A, ids.appA] + ); + expect(row).toEqual({ queue_name: 'email', priority: 5, max_attempts: 3 }); + }); + + it('routing(): another database definition id yields no row', async () => { + const rows = await pg.any( + `SELECT * FROM function_resolution.routing($1, $2)`, + [DB_A, ids.appB] + ); + expect(rows).toEqual([]); + }); + + it('routing(): an all-null routing row falls back to queue defaults', async () => { + const rows = await pg.any( + `SELECT * FROM function_resolution.routing($1, $2)`, + [DB_A, ids.dbScopedA] + ); + expect(rows).toEqual([]); + }); + + it('routing(): a definition outside the published plane yields no row', async () => { + // The row is in LEGACY_DB's own plane, which routing does not read: queue + // defaults apply rather than a value from an unpublished plane. + const rows = await pg.any( + `SELECT * FROM function_resolution.routing($1, $2)`, + [LEGACY_DB, ids.legacy] + ); + expect(rows).toEqual([]); + }); + + it('resolve(): the plane is authoritative — removing the row stops resolution', async () => { + await pg.query(`DELETE FROM catalog_private.functions WHERE id = $1`, [ids.appA]); + await expect( + pg.any( + `SELECT * FROM function_resolution.resolve($1, 'app', NULL, 'email:send', true)`, + [DB_A] + ) + ).rejects.toThrow(/FUNCTION_DEFINITION_NOT_FOUND/); + + await pg.query( + `INSERT INTO catalog_private.functions + (id, owner_scope, owner_key, is_visible, database_id, task_identifier, + queue_name, priority, max_attempts) + VALUES ($1, 'app', NULL, true, $2, 'email:send', 'email', 5, 3)`, + [ids.appA, DB_A] + ); + }); + + it('EXPLAIN: the static probe uses the database-qualified unique index', async () => { + const rows = await pg.any( + `EXPLAIN (FORMAT text) + SELECT hit.id + FROM unnest($1::text[], $2::uuid[], $3::uuid[], $4::bigint[]) + AS cand(owner_scope, owner_key, lookup_database_id, ord) + CROSS JOIN LATERAL ( + SELECT c.id FROM catalog_private.functions c + WHERE c.task_identifier = $5 + AND c.owner_scope = cand.owner_scope + AND c.owner_key = cand.owner_key + AND cand.owner_key IS NOT NULL + AND c.database_id = CASE + WHEN cand.owner_scope = 'database' THEN cand.owner_key + ELSE cand.lookup_database_id + END + UNION ALL + SELECT c.id FROM catalog_private.functions c + WHERE c.task_identifier = $5 + AND c.owner_scope = cand.owner_scope + AND c.owner_key IS NULL + AND cand.owner_key IS NULL + AND c.database_id = cand.lookup_database_id + ) hit + ORDER BY cand.ord + LIMIT 1`, + [['app'], [null], [DB_A], ['1'], 'email:send'] + ); + const plan = rows.map((r: Record) => Object.values(r)[0]).join('\n'); + expect(plan).toMatch(/functions_db_owner_scope_task_idx/); + }); +}); diff --git a/packages/function-resolution/__tests__/resolution.test.ts b/packages/function-resolution/__tests__/resolution.test.ts index ca2beb3e1..53d793b19 100644 --- a/packages/function-resolution/__tests__/resolution.test.ts +++ b/packages/function-resolution/__tests__/resolution.test.ts @@ -119,27 +119,32 @@ describe('function-resolution end-to-end (format-based, no AST)', () => { ); // --- Typed functions catalog (the resolver's read path) ----------------- - await pg.query(`CREATE SCHEMA cat_defs`); + await pg.query(`CREATE SCHEMA catalog_private`); await pg.query( - `CREATE TABLE cat_defs.functions ( + `CREATE TABLE catalog_private.functions ( id uuid PRIMARY KEY, owner_scope text NOT NULL, owner_key uuid, is_visible boolean NOT NULL DEFAULT false, database_id uuid NOT NULL, - task_identifier text NOT NULL + task_identifier text NOT NULL, + queue_name text, + priority integer, + max_attempts integer )` ); await pg.query( - `INSERT INTO cat_defs.functions (id, owner_scope, owner_key, is_visible, database_id, task_identifier) - VALUES ($1, 'app', NULL, false, $4, 'email:send'), - ($2, 'database', $4, false, $4, 'report:run'), - ($3, 'database', NULL, false, $4, 'report:run')`, + `INSERT INTO catalog_private.functions + (id, owner_scope, owner_key, is_visible, database_id, task_identifier, + queue_name, priority, max_attempts) + VALUES ($1, 'app', NULL, false, $4, 'email:send', 'emails', 5, 3), + ($2, 'database', $4, false, $4, 'report:run', 'reports', 9, 10), + ($3, 'database', NULL, false, $4, 'report:run', 'reports_default', 1, 2)`, [ids.appDef, ids.dbExact, ids.dbDefault, TENANT_DB] ); const catSchema = await pg.one( `INSERT INTO metaschema_public.schema (database_id, name, schema_name) - VALUES ($1, 'cat_defs', 'cat_defs') RETURNING id`, + VALUES ($1, 'catalog_private', 'catalog_private') RETURNING id`, [TENANT_DB] ); const catTable = await pg.one( @@ -154,8 +159,9 @@ describe('function-resolution end-to-end (format-based, no AST)', () => { resources_table_id, resource_definitions_table_id, resource_installations_table_id, apps_table_id, buckets_table_id, sites_web_config_table_id, sites_error_pages_table_id, - sites_app_links_table_id, sites_deep_links_table_id, scope) - VALUES ($1, $2, $3, $3, $3, $3, $3, $3, $3, $3, $3, $3, $3, $3, $3, $3, 'app')`, + sites_app_links_table_id, sites_deep_links_table_id, + bindings_table_id, scope) + VALUES ($1, $2, $3, $3, $3, $3, $3, $3, $3, $3, $3, $3, $3, $3, $3, $3, $3, 'app')`, [TENANT_DB, catSchema.id, catTable.id] ); }); @@ -196,6 +202,8 @@ describe('function-resolution end-to-end (format-based, no AST)', () => { ); expect(rows).toEqual([ { scope: 'app', lookup_database_id: TENANT_DB, key_value: null }, + // the tenant's OWN database frame precedes the platform fall-through + { scope: 'database', lookup_database_id: TENANT_DB, key_value: TENANT_DB }, { scope: 'database', lookup_database_id: PLATFORM_DB, key_value: TENANT_DB }, { scope: 'org', lookup_database_id: PLATFORM_DB, key_value: PLATFORM_ORG_ID }, { scope: 'app', lookup_database_id: PLATFORM_DB, key_value: null }, @@ -226,7 +234,7 @@ describe('function-resolution end-to-end (format-based, no AST)', () => { it('resolve(): falls back to the scope-default (owner_key IS NULL) row', async () => { // Without the exact-key catalog row, the same frame's scope-default // (owner_key IS NULL) row wins instead. - await pg.query(`DELETE FROM cat_defs.functions WHERE id = $1`, [ids.dbExact]); + await pg.query(`DELETE FROM catalog_private.functions WHERE id = $1`, [ids.dbExact]); const [row] = await pg.any( `SELECT function_definition_id, resolved_scope FROM function_resolution.resolve($1, 'database', NULL, 'report:run', true)`, @@ -234,8 +242,10 @@ describe('function-resolution end-to-end (format-based, no AST)', () => { ); expect(row).toEqual({ function_definition_id: ids.dbDefault, resolved_scope: 'database' }); await pg.query( - `INSERT INTO cat_defs.functions (id, owner_scope, owner_key, is_visible, database_id, task_identifier) - VALUES ($1, 'database', $2, false, $2, 'report:run')`, + `INSERT INTO catalog_private.functions + (id, owner_scope, owner_key, is_visible, database_id, task_identifier, + queue_name, priority, max_attempts) + VALUES ($1, 'database', $2, false, $2, 'report:run', 'reports', 9, 10)`, [ids.dbExact, TENANT_DB] ); }); @@ -243,7 +253,7 @@ describe('function-resolution end-to-end (format-based, no AST)', () => { it('routing(): loads queue metadata from the resolved definition', async () => { const [row] = await pg.any( `SELECT queue_name, priority, max_attempts - FROM function_resolution.routing($1, 'app', $2)`, + FROM function_resolution.routing($1, $2)`, [TENANT_DB, ids.appDef] ); expect(row).toEqual({ queue_name: 'emails', priority: 5, max_attempts: 3 }); diff --git a/packages/function-resolution/deploy/schemas/function_resolution/procedures/api_catalog_row.sql b/packages/function-resolution/deploy/schemas/function_resolution/procedures/api_catalog_row.sql new file mode 100644 index 000000000..f805f921c --- /dev/null +++ b/packages/function-resolution/deploy/schemas/function_resolution/procedures/api_catalog_row.sql @@ -0,0 +1,64 @@ +-- Deploy schemas/function_resolution/procedures/api_catalog_row to pg + +-- requires: schemas/function_resolution/schema +-- requires: schemas/function_resolution/procedures/frame_candidates + +BEGIN; + +-- api_catalog_row: load one api surface the execution is allowed to reach, by id. +-- +-- Same reachability rule the bucket path uses: the api must be owned by a frame +-- in the execution's own chain, and an api owned by another database +-- additionally needs is_visible. This is what keeps an explicit api binding — +-- or a module attachment resolved in an outer frame — from handing a function +-- another tenant's surface. +-- +-- One indexed read: the frame candidates join catalog_private.apis, nearest +-- frame first. The shared plane holds every database's rows, so each candidate +-- carries the row's expected database_id (its own key at database scope, the +-- frame's lookup database otherwise) — without it a binding could be proved +-- against another tenant's surface. +-- +-- Returns no row when the api is not reachable; the caller owns the wording, +-- because it knows the selector that named it. +-- +-- plpgsql, not sql: this module is portable and deploys into databases that host +-- no catalog module, so catalog_private must be resolved on first call rather +-- than at CREATE FUNCTION time. +CREATE FUNCTION function_resolution.api_catalog_row( + database_id uuid, + scope text, + entity_id uuid, + api_id uuid +) RETURNS TABLE ( + api_name text, + owner_database_id uuid, + owner_scope text, + owner_key uuid +) AS $$ +BEGIN + RETURN QUERY + SELECT a.name, + a.database_id, + a.owner_scope, + a.owner_key + FROM function_resolution.frame_candidates( + api_catalog_row.database_id, + api_catalog_row.scope, + api_catalog_row.entity_id + ) cand + JOIN catalog_private.apis a + ON a.owner_scope = cand.owner_scope + AND a.owner_key IS NOT DISTINCT FROM cand.owner_key + AND a.database_id = CASE + WHEN cand.owner_scope = 'database' THEN cand.owner_key + ELSE cand.lookup_database_id + END + WHERE a.id = api_catalog_row.api_id + AND (a.database_id = api_catalog_row.database_id OR a.is_visible) + ORDER BY cand.ord + LIMIT 1; +END; +$$ LANGUAGE plpgsql STABLE SECURITY DEFINER; + +COMMIT; diff --git a/packages/function-resolution/deploy/schemas/function_resolution/procedures/bound_bucket_id.sql b/packages/function-resolution/deploy/schemas/function_resolution/procedures/bound_bucket_id.sql new file mode 100644 index 000000000..797d24fd7 --- /dev/null +++ b/packages/function-resolution/deploy/schemas/function_resolution/procedures/bound_bucket_id.sql @@ -0,0 +1,81 @@ +-- Deploy schemas/function_resolution/procedures/bound_bucket_id to pg + +-- requires: schemas/function_resolution/schema +-- requires: schemas/function_resolution/procedures/frame_candidates + +BEGIN; + +-- bound_bucket_id: the bucket a capability binding fulfils one declared key +-- with, or NULL when the tenant left the key to discovery. +-- +-- A binding is the deterministic override: it exists precisely for the cases +-- discovery cannot answer — two buckets share a tag, or a tenant wants an +-- audited, explicit grant instead of a labelling convention. It is therefore +-- consulted before tags, and the row nearest the execution wins: candidates are +-- ordered most-specific first, matching the frame's scope key so one entity's +-- grant never leaks to another. +-- +-- Lifecycle orders the tie within one frame: an execution-scoped binding is +-- more specific than a root-execution one, which is more specific than a +-- deployment-wide one. ('deployment' | 'execution' | 'root_execution' are the +-- lifecycle values function_module's check constraint allows — when a capability +-- applies, not a scope.) +-- +-- The shared plane holds every database's rows, so each candidate carries the +-- row's expected database_id (its own key at database scope, the frame's lookup +-- database otherwise): without it, a binding written by another tenant at the +-- same (scope, key) coordinates would answer this execution. +-- +-- No is_visible predicate, unlike the buckets and apis catalogs: a binding is +-- not a cross-scope claim. It is reachable only through a frame of the +-- execution that holds it, which is exactly the authority the scoped source +-- table carried — so the projection is read with the frame identity alone, and +-- nothing about visibility widens or narrows it. +-- +-- The bucket returned here is NOT yet proven reachable — bucket_catalog_row +-- does that. Keeping the two apart is what lets the caller name the capability +-- key in the error. +-- +-- plpgsql, not sql: this module is portable and deploys into databases that host +-- no catalog module, so catalog_private must be resolved on first call rather +-- than at CREATE FUNCTION time. +CREATE FUNCTION function_resolution.bound_bucket_id( + database_id uuid, + scope text, + entity_id uuid, + function_definition_id uuid, + key text +) RETURNS uuid AS $$ +DECLARE + v_bucket_id uuid; +BEGIN + SELECT b.bucket_id + INTO v_bucket_id + FROM function_resolution.frame_candidates( + bound_bucket_id.database_id, + bound_bucket_id.scope, + bound_bucket_id.entity_id + ) cand + JOIN catalog_private.bindings b + ON b.owner_scope = cand.owner_scope + AND b.owner_key IS NOT DISTINCT FROM cand.owner_key + AND b.database_id = CASE + WHEN cand.owner_scope = 'database' THEN cand.owner_key + ELSE cand.lookup_database_id + END + WHERE b.function_id = bound_bucket_id.function_definition_id + AND b.key = bound_bucket_id.key + AND b.bucket_id IS NOT NULL + ORDER BY cand.ord, + CASE b.lifecycle + WHEN 'execution' THEN 0 + WHEN 'root_execution' THEN 1 + ELSE 2 + END + LIMIT 1; + + RETURN v_bucket_id; +END; +$$ LANGUAGE plpgsql STABLE SECURITY DEFINER; + +COMMIT; diff --git a/packages/function-resolution/deploy/schemas/function_resolution/procedures/bucket_catalog_row.sql b/packages/function-resolution/deploy/schemas/function_resolution/procedures/bucket_catalog_row.sql new file mode 100644 index 000000000..8c205e63f --- /dev/null +++ b/packages/function-resolution/deploy/schemas/function_resolution/procedures/bucket_catalog_row.sql @@ -0,0 +1,72 @@ +-- Deploy schemas/function_resolution/procedures/bucket_catalog_row to pg + +-- requires: schemas/function_resolution/schema +-- requires: schemas/function_resolution/procedures/frame_candidates + +BEGIN; + +-- bucket_catalog_row: load one bucket the execution is allowed to reach, by id. +-- +-- This is where a capability binding's target is proved to belong to the +-- caller's tenant. The generated binding guard cannot do it: compute's +-- published platform modules must stay self-contained, so nothing generated +-- into compute may reference a storage table. Resolution can — the binding is +-- honoured only when the bucket is reachable from the execution's own frame +-- chain, and a bucket owned by another database additionally needs is_visible. +-- A binding pointing anywhere else returns nothing rather than handing a +-- function a foreign tenant's storage. +-- +-- One indexed read: the frame candidates join catalog_private.buckets, nearest +-- frame first. The shared plane holds every database's rows, so each candidate +-- carries the row's expected database_id (its own key at database scope, the +-- frame's lookup database otherwise) — without it a binding could be "proved" +-- against a row belonging to another tenant, which is the exact hazard this +-- function exists to stop. +-- +-- Returns no row when the bucket is not reachable, leaving the fail-loud +-- wording to the caller, which knows the capability key that named it. +-- +-- plpgsql, not sql: this module is portable and deploys into databases that host +-- no catalog module, so catalog_private must be resolved on first call rather +-- than at CREATE FUNCTION time. +CREATE FUNCTION function_resolution.bucket_catalog_row( + database_id uuid, + scope text, + entity_id uuid, + bucket_id uuid +) RETURNS TABLE ( + bucket_key text, + bucket_type text, + physical_name text, + owner_database_id uuid, + owner_scope text, + owner_key uuid +) AS $$ +BEGIN + RETURN QUERY + SELECT b.key, + b.type, + b.physical_name, + b.database_id, + b.owner_scope, + b.owner_key + FROM function_resolution.frame_candidates( + bucket_catalog_row.database_id, + bucket_catalog_row.scope, + bucket_catalog_row.entity_id + ) cand + JOIN catalog_private.buckets b + ON b.owner_scope = cand.owner_scope + AND b.owner_key IS NOT DISTINCT FROM cand.owner_key + AND b.database_id = CASE + WHEN cand.owner_scope = 'database' THEN cand.owner_key + ELSE cand.lookup_database_id + END + WHERE b.id = bucket_catalog_row.bucket_id + AND (b.database_id = bucket_catalog_row.database_id OR b.is_visible) + ORDER BY cand.ord + LIMIT 1; +END; +$$ LANGUAGE plpgsql STABLE SECURITY DEFINER; + +COMMIT; diff --git a/packages/function-resolution/deploy/schemas/function_resolution/procedures/catalog_location.sql b/packages/function-resolution/deploy/schemas/function_resolution/procedures/catalog_location.sql deleted file mode 100644 index 0ef173fa8..000000000 --- a/packages/function-resolution/deploy/schemas/function_resolution/procedures/catalog_location.sql +++ /dev/null @@ -1,56 +0,0 @@ --- Deploy schemas/function_resolution/procedures/catalog_location to pg --- requires: schemas/function_resolution/schema --- requires: metaschema-schema:schemas/metaschema_public/tables/table/table --- requires: metaschema-modules:schemas/metaschema_modules_public/tables/catalog_module/table - -BEGIN; - --- catalog_location: deterministically resolve the typed functions-catalog --- table (schema, table) for one database. The catalog is the flat, --- trigger-maintained projection every function definition of every scope --- hosted in that database registers into (catalog_module + catalog_register), --- keyed by (owner_scope, owner_key, task_identifier). --- --- Returns no row when the database has no catalog module or its catalog has --- no functions table — callers treat that as "catalog resolution unavailable --- for this database" and decide (fail loud vs frame-walk fallback) at their --- layer. More than one catalog module for one database is a provisioning bug --- and raises. -CREATE FUNCTION function_resolution.catalog_location( - database_id uuid -) RETURNS TABLE ( - schema_name text, - table_name text -) AS $$ -DECLARE - v_functions_table_id uuid; -BEGIN - BEGIN - SELECT cm.functions_table_id - INTO STRICT v_functions_table_id - FROM metaschema_modules_public.catalog_module cm - WHERE cm.database_id = catalog_location.database_id - AND cm.functions_table_id <> uuid_nil(); - EXCEPTION - WHEN NO_DATA_FOUND THEN - RETURN; - WHEN TOO_MANY_ROWS THEN - RAISE EXCEPTION 'FUNCTION_RESOLUTION_CATALOG_AMBIGUOUS: multiple functions catalogs registered for database %', - catalog_location.database_id; - END; - - SELECT s.schema_name, t.name - INTO catalog_location.schema_name, catalog_location.table_name - FROM metaschema_public.schema s - JOIN metaschema_public."table" t ON (t.schema_id = s.id AND t.database_id = s.database_id) - WHERE t.id = v_functions_table_id; - - IF NOT FOUND THEN - RETURN; - END IF; - - RETURN NEXT; -END; -$$ LANGUAGE plpgsql STABLE SECURITY DEFINER; - -COMMIT; diff --git a/packages/function-resolution/deploy/schemas/function_resolution/procedures/enqueue.sql b/packages/function-resolution/deploy/schemas/function_resolution/procedures/enqueue.sql index b0a8c24ee..711691c7b 100644 --- a/packages/function-resolution/deploy/schemas/function_resolution/procedures/enqueue.sql +++ b/packages/function-resolution/deploy/schemas/function_resolution/procedures/enqueue.sql @@ -125,7 +125,7 @@ BEGIN SELECT rt.queue_name, rt.priority, rt.max_attempts INTO v_queue_name, v_priority, v_max_attempts - FROM function_resolution.routing(v_defs_db, v_def_scope, v_fn_id) rt; + FROM function_resolution.routing(v_defs_db, v_fn_id) rt; END IF; -- Caller-supplied routing always wins over the definition's; the definition's diff --git a/packages/function-resolution/deploy/schemas/function_resolution/procedures/frame_candidates.sql b/packages/function-resolution/deploy/schemas/function_resolution/procedures/frame_candidates.sql new file mode 100644 index 000000000..960db7912 --- /dev/null +++ b/packages/function-resolution/deploy/schemas/function_resolution/procedures/frame_candidates.sql @@ -0,0 +1,50 @@ +-- Deploy schemas/function_resolution/procedures/frame_candidates to pg + +-- requires: schemas/function_resolution/schema +-- requires: pgpm-app-scope:schemas/app_scope/procedures/frames + +BEGIN; + +-- frame_candidates: expand the ordered frames of one execution into the +-- (owner_scope, owner_key) probe candidates a typed catalog is keyed by, +-- most-specific first. +-- +-- app_scope.frames stays the single source of truth for ordering (and +-- cycle/depth safety); this only performs the expansion every catalog probe +-- needs, so the capability resolvers never hand-order scopes: +-- * global frame (key_value NULL): (scope, owner_key IS NULL) +-- * keyed frame: (scope, owner_key = key) -- most specific +-- then (scope, owner_key IS NULL) -- scope default +-- ord is a global ordinality across frames, so the lowest ord that matches wins +-- regardless of which frame database answered. +CREATE FUNCTION function_resolution.frame_candidates( + database_id uuid, + scope text, + entity_id uuid DEFAULT NULL +) RETURNS TABLE ( + lookup_database_id uuid, + owner_scope text, + owner_key uuid, + ord bigint +) AS $$ +BEGIN + RETURN QUERY + SELECT f.lookup_database_id, + f.scope, + cand.owner_key, + (f.ord * 2) + cand.off + FROM app_scope.frames( + frame_candidates.database_id, + frame_candidates.scope, + frame_candidates.entity_id + ) WITH ORDINALITY AS f(scope, lookup_database_id, key_value, ord) + CROSS JOIN LATERAL ( + VALUES (f.key_value, 0::bigint), (NULL::uuid, 1::bigint) + ) AS cand(owner_key, off) + -- Global frames carry no key: emit the NULL candidate once. + WHERE cand.off = 0 OR f.key_value IS NOT NULL + ORDER BY (f.ord * 2) + cand.off; +END; +$$ LANGUAGE plpgsql STABLE SECURITY DEFINER; + +COMMIT; diff --git a/packages/function-resolution/deploy/schemas/function_resolution/procedures/resolve.sql b/packages/function-resolution/deploy/schemas/function_resolution/procedures/resolve.sql index 3b73e7bdf..acd2b34fe 100644 --- a/packages/function-resolution/deploy/schemas/function_resolution/procedures/resolve.sql +++ b/packages/function-resolution/deploy/schemas/function_resolution/procedures/resolve.sql @@ -1,37 +1,42 @@ -- Deploy schemas/function_resolution/procedures/resolve to pg -- requires: schemas/function_resolution/schema --- requires: schemas/function_resolution/procedures/catalog_location -- requires: metaschema-modules:schemas/metaschema_modules_public/tables/function_module/table +-- requires: metaschema-modules:schemas/metaschema_modules_public/tables/catalog_module/table -- requires: pgpm-app-scope:schemas/app_scope/procedures/frames BEGIN; -- resolve: deterministic cross-scope function resolver, answered from the --- typed functions catalog. +-- published functions catalog in ONE static indexed read. -- -- It expands the ordered frames from app_scope.frames into --- (owner_scope, owner_key) candidates and runs ONE indexed read against the --- typed functions catalog of each DISTINCT frame database (at most two in any --- real chain: the execution database and the platform database). --- app_scope.frames stays the single source of truth for ordering (and --- cycle/depth safety). +-- (owner_scope, owner_key) candidates and probes catalog_private.functions once +-- for all of them. app_scope.frames stays the single source of truth for +-- ordering (and cycle/depth safety). -- -- Candidate expansion per frame: -- * global frame (key_value NULL): (scope, owner_key IS NULL) -- * keyed frame: (scope, owner_key = key) -- most specific -- then (scope, owner_key IS NULL) -- scope default --- The catalog query is a LATERAL over the ordered candidate list with one +-- The query is a LATERAL over the ordered candidate list with one -- exact-equality branch per owner_key nullness, so each candidate is a single -- probe of the catalog's partial unique indexes --- ((owner_scope, owner_key, task_identifier) WHERE owner_key IS NOT NULL and --- (owner_scope, task_identifier) WHERE owner_key IS NULL). Lowest candidate --- ordinality across all frame databases wins. +-- ((database_id, owner_scope, owner_key, task_identifier) WHERE owner_key IS NOT +-- NULL and (database_id, owner_scope, task_identifier) WHERE owner_key IS NULL). +-- Lowest candidate ordinality wins, which is frame precedence. -- --- Availability is fail-loud, never silent: a frame database that hosts --- function modules but has no functions catalog cannot be answered, so the --- typed FUNCTION_RESOLUTION_CATALOG_UNAVAILABLE (SQLSTATE FR001) is raised. --- A frame database with neither function modules nor a catalog contributes --- nothing and is skipped. +-- One physical relation holds every database's rows, so each candidate carries +-- the row-identity predicate that plane needs: a database-scope row stamps its +-- own database key (metaschema_generators.scope_key_column: only 'database' has +-- one), every other scope stamps the writing session's database, so the expected +-- database_id is the candidate's owner_key at database scope and the frame's +-- lookup database otherwise. Without it the shared plane would answer one +-- tenant's probe with another tenant's row. +-- +-- Availability is fail-loud, never silent: a frame database that hosts function +-- modules but has no catalog module cannot be answered, so the typed +-- FUNCTION_RESOLUTION_CATALOG_UNAVAILABLE (SQLSTATE FR001) is raised. A frame +-- database with neither contributes nothing and is skipped. -- -- owner_database_id is the catalog row's owning database — the definition's -- home database — so callers (routing) need no second frame walk to find it. @@ -47,101 +52,88 @@ CREATE FUNCTION function_resolution.resolve( owner_database_id uuid ) AS $$ DECLARE - v_frame_db record; - v_schema text; - v_table text; - v_query text; + v_unanswerable uuid; v_hit record; - v_best_ord bigint; BEGIN - -- Group the ordered frames by lookup database, expanding each frame into - -- its probe candidates (exact key first, scope-default second) with a - -- global ordinality that preserves frame precedence across databases. - FOR v_frame_db IN - SELECT c.lookup_database_id, - array_agg(c.owner_scope ORDER BY c.ord) AS scopes, - array_agg(c.owner_key ORDER BY c.ord) AS keys, - array_agg(c.ord ORDER BY c.ord) AS ords - FROM ( - SELECT f.lookup_database_id, - f.scope AS owner_scope, - cand.owner_key, - (f.ord * 2) + cand.off AS ord - FROM app_scope.frames( - resolve.database_id, - resolve.scope, - resolve.entity_id - ) WITH ORDINALITY AS f(scope, lookup_database_id, key_value, ord) - CROSS JOIN LATERAL ( - VALUES (f.key_value, 0::bigint), (NULL::uuid, 1::bigint) - ) AS cand(owner_key, off) - -- Global frames carry no key: emit the NULL candidate once. - WHERE cand.off = 0 OR f.key_value IS NOT NULL - ) c - GROUP BY c.lookup_database_id - ORDER BY min(c.ord) - LOOP - SELECT l.schema_name, l.table_name - INTO v_schema, v_table - FROM function_resolution.catalog_location(v_frame_db.lookup_database_id) l; - - IF v_schema IS NULL THEN - -- No catalog for this frame database. If it hosts function - -- modules the catalog cannot answer for it — fail loud so a - -- missing catalog never silently mis-resolves as "not found". - IF EXISTS ( - SELECT 1 FROM metaschema_modules_public.function_module fm - WHERE fm.database_id = v_frame_db.lookup_database_id - ) THEN - RAISE EXCEPTION USING - errcode = 'FR001', - message = format( - 'FUNCTION_RESOLUTION_CATALOG_UNAVAILABLE: database %s hosts function modules but has no functions catalog', - v_frame_db.lookup_database_id - ); - END IF; - CONTINUE; - END IF; + -- A frame database that hosts function modules but never deployed a catalog + -- module has definitions the catalog cannot see. Answering "not found" there + -- would be a wrong answer, so it is raised before any probe. + SELECT f.lookup_database_id + INTO v_unanswerable + FROM app_scope.frames( + resolve.database_id, + resolve.scope, + resolve.entity_id + ) AS f(scope, lookup_database_id, key_value) + WHERE EXISTS ( + SELECT 1 FROM metaschema_modules_public.function_module fm + WHERE fm.database_id = f.lookup_database_id + ) + AND NOT EXISTS ( + SELECT 1 + FROM metaschema_modules_public.catalog_module cm + WHERE cm.database_id = f.lookup_database_id + AND cm.functions_table_id IS NOT NULL + AND cm.functions_table_id <> uuid_nil() + ) + LIMIT 1; - -- One indexed read per catalog: LATERAL over the ordered candidates, - -- each branch an exact probe of one partial unique index. - v_query := format( - 'SELECT hit.id, hit.owner_scope, hit.database_id, cand.ord - FROM unnest($2::text[], $3::uuid[], $4::bigint[]) AS cand(owner_scope, owner_key, ord) - CROSS JOIN LATERAL ( - SELECT c.id, c.owner_scope, c.database_id - FROM %I.%I c - WHERE c.task_identifier = $1 - AND c.owner_scope = cand.owner_scope - AND c.owner_key = cand.owner_key - AND cand.owner_key IS NOT NULL - UNION ALL - SELECT c.id, c.owner_scope, c.database_id - FROM %I.%I c - WHERE c.task_identifier = $1 - AND c.owner_scope = cand.owner_scope - AND c.owner_key IS NULL - AND cand.owner_key IS NULL - ) hit - ORDER BY cand.ord - LIMIT 1', - v_schema, v_table, v_schema, v_table - ); - - EXECUTE v_query - INTO v_hit - USING resolve.task_identifier, - v_frame_db.scopes, v_frame_db.keys, v_frame_db.ords; + IF v_unanswerable IS NOT NULL THEN + RAISE EXCEPTION USING + errcode = 'FR001', + message = format( + 'FUNCTION_RESOLUTION_CATALOG_UNAVAILABLE: database %s hosts function modules but has no functions catalog', + v_unanswerable + ); + END IF; - IF v_hit.id IS NOT NULL AND (v_best_ord IS NULL OR v_hit.ord < v_best_ord) THEN - v_best_ord := v_hit.ord; - resolve.function_definition_id := v_hit.id; - resolve.resolved_scope := v_hit.owner_scope; - resolve.owner_database_id := v_hit.database_id; - END IF; - END LOOP; + -- One indexed read: LATERAL over the ordered candidates, each branch an + -- exact probe of one partial unique index. + SELECT hit.id, hit.owner_scope, hit.database_id + INTO v_hit + FROM ( + SELECT f.lookup_database_id, + f.scope AS owner_scope, + cand.owner_key, + (f.ord * 2) + cand.off AS ord + FROM app_scope.frames( + resolve.database_id, + resolve.scope, + resolve.entity_id + ) WITH ORDINALITY AS f(scope, lookup_database_id, key_value, ord) + CROSS JOIN LATERAL ( + VALUES (f.key_value, 0::bigint), (NULL::uuid, 1::bigint) + ) AS cand(owner_key, off) + -- Global frames carry no key: emit the NULL candidate once. + WHERE cand.off = 0 OR f.key_value IS NOT NULL + ) cand + CROSS JOIN LATERAL ( + SELECT c.id, c.owner_scope, c.database_id + FROM catalog_private.functions c + WHERE c.task_identifier = resolve.task_identifier + AND c.owner_scope = cand.owner_scope + AND c.owner_key = cand.owner_key + AND cand.owner_key IS NOT NULL + AND c.database_id = CASE + WHEN cand.owner_scope = 'database' THEN cand.owner_key + ELSE cand.lookup_database_id + END + UNION ALL + SELECT c.id, c.owner_scope, c.database_id + FROM catalog_private.functions c + WHERE c.task_identifier = resolve.task_identifier + AND c.owner_scope = cand.owner_scope + AND c.owner_key IS NULL + AND cand.owner_key IS NULL + AND c.database_id = cand.lookup_database_id + ) hit + ORDER BY cand.ord + LIMIT 1; - IF resolve.function_definition_id IS NOT NULL THEN + IF v_hit.id IS NOT NULL THEN + resolve.function_definition_id := v_hit.id; + resolve.resolved_scope := v_hit.owner_scope; + resolve.owner_database_id := v_hit.database_id; RETURN NEXT; RETURN; END IF; diff --git a/packages/function-resolution/deploy/schemas/function_resolution/procedures/resolve_api.sql b/packages/function-resolution/deploy/schemas/function_resolution/procedures/resolve_api.sql new file mode 100644 index 000000000..2dc93b7a2 --- /dev/null +++ b/packages/function-resolution/deploy/schemas/function_resolution/procedures/resolve_api.sql @@ -0,0 +1,367 @@ +-- Deploy schemas/function_resolution/procedures/resolve_api to pg + +-- requires: schemas/function_resolution/schema +-- requires: schemas/function_resolution/procedures/frame_candidates +-- requires: schemas/function_resolution/procedures/api_catalog_row +-- requires: pgpm-app-scope:schemas/app_scope/procedures/routing_tables + +BEGIN; + +-- resolve_api: answer "which api surface does this selector mean for this +-- execution". +-- +-- The selector vocabulary is module names — the same names the module presets +-- and provision_database_modules use — augmented to name a specific api when a +-- module has several, and a scope when a module is registered at several: +-- +-- notifications_module the module's api surface +-- permissions_module.admin this api from this module +-- limits_module@org the module's org-scope registration +-- permissions_module.admin@org both +-- admin an api by its owner-local name +-- +-- Anything ending in _module (before its suffixes) is a module selector; a +-- plain word is an api name — the escape hatch, not the default. +-- +-- The module form is the rename-safe one, and it needs no new column to be so: +-- module registration already attaches the module's schema to the api by row +-- (api_schemas.api_id -> apis.id, written by metaschema.add_schema_to_api), so +-- the association survives any rename of the api. The module config row's +-- api_name stays what it has always been — authoring input naming the surface +-- to attach to (and the tie-breaker when a bare module selector finds several +-- attached surfaces) — not the association itself. +-- +-- Same determinism and reachability rules as resolve_bucket: most-specific +-- frame first, exactly one answer in the winning frame or a raise that names +-- the candidates, and a foreign-database api resolves only when is_visible. +-- +-- The path reads catalog_private.apis statically for every frame at +-- once, carrying each candidate's expected database_id so the shared plane +-- cannot answer one tenant's probe with another's row. +-- +-- The path's dynamic SQL is a different case, and is the only dynamic +-- SQL left in resolution: a module registration table is one relation per module type +-- (there is no catalog kind projecting them), so the selector genuinely names +-- the relation. It is lookup-only, the relation is proved to exist by an +-- ordinary catalog join first, and no value is ever interpolated — only the +-- verified relation name. +CREATE FUNCTION function_resolution.resolve_api( + database_id uuid, + scope text, + entity_id uuid, + selector text +) RETURNS TABLE ( + api_id uuid, + api_name text, + owner_database_id uuid, + owner_scope text, + owner_key uuid +) AS $$ +DECLARE + v_kind text; + v_ref text; + v_api_filter text; + v_scope_filter text; + v_module_table text; + v_module_relid oid; + v_frame record; + v_query text; + v_row record; + v_matches jsonb := '[]'::jsonb; + v_match jsonb; + v_module_schema_id uuid; + v_module_api_name text; + v_module_has_scope boolean; + v_apis_schema text; + v_apis_table text; + v_api_schemas_schema text; + v_api_schemas_table text; + v_resolved record; +BEGIN + IF resolve_api.selector IS NULL OR btrim(resolve_api.selector) = '' THEN + RAISE EXCEPTION 'CAPABILITY_API_SELECTOR_EMPTY: an api selector is required (database_id=%, scope="%")', + resolve_api.database_id, resolve_api.scope + USING ERRCODE = 'FR020'; + END IF; + + -- [.][@] | + v_ref := btrim(resolve_api.selector); + + v_scope_filter := nullif(split_part(v_ref, '@', 2), ''); + v_ref := split_part(v_ref, '@', 1); + + IF split_part(v_ref, '.', 1) LIKE '%\_module' THEN + v_kind := 'module'; + v_api_filter := nullif(substr(v_ref, length(split_part(v_ref, '.', 1)) + 2), ''); + v_ref := split_part(v_ref, '.', 1); + ELSE + v_kind := 'name'; + + IF v_ref LIKE '%.%' OR v_scope_filter IS NOT NULL THEN + RAISE EXCEPTION 'CAPABILITY_API_SELECTOR_INVALID: selector "%" qualifies an api name; only a module selector (_module) takes .api or @scope suffixes', + resolve_api.selector + USING ERRCODE = 'FR020'; + END IF; + END IF; + + IF btrim(coalesce(v_ref, '')) = '' THEN + RAISE EXCEPTION 'CAPABILITY_API_SELECTOR_INVALID: selector "%" names nothing', + resolve_api.selector + USING ERRCODE = 'FR020'; + END IF; + + -- ========================================================================= + -- — one indexed read of the apis catalog for every frame + -- ========================================================================= + IF v_kind = 'name' THEN + -- Keeps only the matches of the most specific frame that answered: a + -- nearer frame outranks an outer one, and ties within that frame are the + -- ambiguity raised below. + WITH hits AS ( + SELECT a.id, + a.name, + a.database_id, + a.owner_scope, + a.owner_key, + cand.ord + FROM function_resolution.frame_candidates( + resolve_api.database_id, + resolve_api.scope, + resolve_api.entity_id + ) cand + JOIN catalog_private.apis a + ON a.owner_scope = cand.owner_scope + AND a.owner_key IS NOT DISTINCT FROM cand.owner_key + AND a.database_id = CASE + WHEN cand.owner_scope = 'database' THEN cand.owner_key + ELSE cand.lookup_database_id + END + WHERE a.name = v_ref + AND (a.database_id = resolve_api.database_id OR a.is_visible) + ), + nearest AS ( + SELECT h.* + FROM hits h + WHERE h.ord = (SELECT min(hh.ord) FROM hits hh) + ) + SELECT COALESCE(jsonb_agg(to_jsonb(n) ORDER BY n.id), '[]'::jsonb) + INTO v_matches + FROM nearest n; + + IF jsonb_array_length(v_matches) = 0 THEN + RAISE EXCEPTION 'CAPABILITY_API_NOT_FOUND: no api named "%" resolves in the scope chain starting at scope "%" (database_id=%)', + v_ref, resolve_api.scope, resolve_api.database_id + USING ERRCODE = 'FR021'; + END IF; + + IF jsonb_array_length(v_matches) > 1 THEN + RAISE EXCEPTION 'CAPABILITY_API_AMBIGUOUS: % apis named "%" resolve equally (candidates: %)', + jsonb_array_length(v_matches), + v_ref, + (SELECT string_agg(format('%s (%s)', m->>'name', m->>'id'), ', ' ORDER BY m->>'id') + FROM jsonb_array_elements(v_matches) m) + USING ERRCODE = 'FR022'; + END IF; + + v_match := v_matches->0; + + resolve_api.api_id := (v_match->>'id')::uuid; + resolve_api.api_name := v_match->>'name'; + resolve_api.owner_database_id := (v_match->>'database_id')::uuid; + resolve_api.owner_scope := v_match->>'owner_scope'; + resolve_api.owner_key := (v_match->>'owner_key')::uuid; + + RETURN NEXT; + RETURN; + END IF; + + -- ========================================================================= + -- [.][@] — resolve through the module's api attachment + -- ========================================================================= + v_module_table := v_ref; + + -- The selector names a module registration table, and a name that is not one + -- must fail as a typo rather than as "not provisioned". Looked up in the + -- catalog by name as an ordinary join: the relation is identified by a bind + -- parameter, so no identifier is interpolated into SQL, and the oid is then + -- reused for the column checks below instead of being probed again. + SELECT c.oid + INTO v_module_relid + FROM pg_class c + JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE n.nspname = 'metaschema_modules_public' + AND c.relname = v_module_table + AND c.relkind IN ('r', 'p', 'v', 'm', 'f'); + + IF v_module_relid IS NULL THEN + RAISE EXCEPTION 'CAPABILITY_API_MODULE_UNKNOWN: selector "%" names module "%", which has no registration table in metaschema_modules_public', + resolve_api.selector, v_ref + USING ERRCODE = 'FR023'; + END IF; + + -- Not every module registration is scoped: a module whose schemas can only + -- be attached at one scope has no scope column, and asking for one turns a + -- valid selector into an error. + SELECT EXISTS ( + SELECT 1 + FROM pg_attribute a + WHERE a.attrelid = v_module_relid + AND a.attname = 'scope' + AND a.attnum > 0 + AND NOT a.attisdropped + ) + INTO v_module_has_scope; + + IF v_scope_filter IS NOT NULL AND NOT v_module_has_scope THEN + RAISE EXCEPTION 'CAPABILITY_API_SELECTOR_INVALID: selector "%" asks for scope "%", but module "%" registers at a single scope', + resolve_api.selector, v_scope_filter, v_ref + USING ERRCODE = 'FR020'; + END IF; + + IF NOT EXISTS ( + SELECT 1 + FROM pg_attribute a + WHERE a.attrelid = v_module_relid + AND a.attname = 'api_name' + AND a.attnum > 0 + AND NOT a.attisdropped + ) THEN + RAISE EXCEPTION 'CAPABILITY_API_MODULE_UNSUPPORTED: module "%" has no api_name column, so selector "%" cannot name one of its surfaces', + v_ref, resolve_api.selector + USING ERRCODE = 'FR023'; + END IF; + + FOR v_frame IN + SELECT f.lookup_database_id, f.scope, min(f.ord) AS ord + FROM function_resolution.frame_candidates( + resolve_api.database_id, + resolve_api.scope, + resolve_api.entity_id + ) f(lookup_database_id, scope, owner_key, ord) + GROUP BY f.lookup_database_id, f.scope + ORDER BY min(f.ord) + LOOP + -- @scope pins the registration to one scope tier; without it, the + -- frame being walked supplies the scope, so a department execution + -- finds its org's registration when the org frame comes up. + -- pgsql-lint-disable-next-line no-dynamic-sql -- lookup-only: the module registration table is named by the selector + v_query := format( + 'SELECT m.schema_id, m.api_name + FROM metaschema_modules_public.%I m + WHERE m.database_id = $1 + %s + LIMIT 1', + v_module_table, + CASE WHEN v_module_has_scope THEN 'AND m.scope = $2' ELSE 'AND $2 IS NOT NULL' END + ); + + CONTINUE WHEN v_scope_filter IS NOT NULL AND v_scope_filter <> v_frame.scope; + + EXECUTE v_query + INTO v_module_schema_id, v_module_api_name + USING v_frame.lookup_database_id, v_frame.scope; + + CONTINUE WHEN v_module_schema_id IS NULL; + + SELECT r.apis_schema, r.apis_table, r.api_schemas_schema, r.api_schemas_table + INTO v_apis_schema, v_apis_table, v_api_schemas_schema, v_api_schemas_table + FROM app_scope.routing_tables(v_frame.lookup_database_id, v_frame.scope) r; + + CONTINUE WHEN v_apis_schema IS NULL OR v_api_schemas_schema IS NULL; + + -- pgsql-lint-disable-next-line no-dynamic-sql -- lookup-only: the api surface tables are located per frame + v_query := format( + 'SELECT s.api_id AS id, a.name + FROM %I.%I s + JOIN %I.%I a ON a.id = s.api_id + WHERE s.schema_id = $1', + v_api_schemas_schema, v_api_schemas_table, + v_apis_schema, v_apis_table + ); + + v_matches := '[]'::jsonb; + + FOR v_row IN EXECUTE v_query USING v_module_schema_id + LOOP + v_matches := v_matches || to_jsonb(v_row); + END LOOP; + + CONTINUE WHEN jsonb_array_length(v_matches) = 0; + + -- .api names the surface outright; a bare module selector finding + -- several surfaces falls back to the module row's own api_name (the + -- authored intent). Failing both, the selector is genuinely ambiguous + -- and must not guess. + v_match := NULL; + + IF v_api_filter IS NOT NULL THEN + SELECT m INTO v_match + FROM jsonb_array_elements(v_matches) m + WHERE m->>'name' = v_api_filter; + + IF v_match IS NULL THEN + RAISE EXCEPTION 'CAPABILITY_API_NOT_FOUND: module "%" has no attached api named "%" (candidates: %)', + v_ref, + v_api_filter, + (SELECT string_agg(format('%s (%s)', m->>'name', m->>'id'), ', ' ORDER BY m->>'id') + FROM jsonb_array_elements(v_matches) m) + USING ERRCODE = 'FR021'; + END IF; + ELSIF jsonb_array_length(v_matches) > 1 THEN + SELECT m INTO v_match + FROM jsonb_array_elements(v_matches) m + WHERE m->>'name' = v_module_api_name; + + IF v_match IS NULL THEN + RAISE EXCEPTION 'CAPABILITY_API_AMBIGUOUS: module "%" has schemas attached to % apis (candidates: %) and none matches its api_name "%"; name one with %.', + v_ref, + jsonb_array_length(v_matches), + (SELECT string_agg(format('%s (%s)', m->>'name', m->>'id'), ', ' ORDER BY m->>'id') + FROM jsonb_array_elements(v_matches) m), + coalesce(v_module_api_name, ''), + v_ref + USING ERRCODE = 'FR022'; + END IF; + ELSE + v_match := v_matches->0; + END IF; + + SELECT * + INTO v_resolved + FROM function_resolution.api_catalog_row( + resolve_api.database_id, + resolve_api.scope, + resolve_api.entity_id, + (v_match->>'id')::uuid + ); + + -- The attachment names an api the execution cannot reach (another + -- tenant's, or an outer frame's unpublished surface): that is a + -- misconfiguration, not a reason to fall through to a further frame. + IF NOT FOUND THEN + RAISE EXCEPTION 'CAPABILITY_API_UNREACHABLE: selector "%" resolves to api % via module "%", which is not visible to database %', + resolve_api.selector, v_match->>'id', v_ref, resolve_api.database_id + USING ERRCODE = 'FR021'; + END IF; + + resolve_api.api_id := (v_match->>'id')::uuid; + resolve_api.api_name := v_resolved.api_name; + resolve_api.owner_database_id := v_resolved.owner_database_id; + resolve_api.owner_scope := v_resolved.owner_scope; + resolve_api.owner_key := v_resolved.owner_key; + + RETURN NEXT; + RETURN; + END LOOP; + + RAISE EXCEPTION 'CAPABILITY_API_NOT_FOUND: selector "%" resolves no api surface in the scope chain starting at scope "%" (database_id=%): module "%" is either unregistered there%s or has no api attachment', + resolve_api.selector, + resolve_api.scope, + resolve_api.database_id, + v_ref, + CASE WHEN v_scope_filter IS NULL THEN '' ELSE format(' at scope "%s"', v_scope_filter) END + USING ERRCODE = 'FR021'; +END; +$$ LANGUAGE plpgsql STABLE SECURITY DEFINER; + +COMMIT; diff --git a/packages/function-resolution/deploy/schemas/function_resolution/procedures/resolve_bucket.sql b/packages/function-resolution/deploy/schemas/function_resolution/procedures/resolve_bucket.sql new file mode 100644 index 000000000..6a8afa178 --- /dev/null +++ b/packages/function-resolution/deploy/schemas/function_resolution/procedures/resolve_bucket.sql @@ -0,0 +1,133 @@ +-- Deploy schemas/function_resolution/procedures/resolve_bucket to pg + +-- requires: schemas/function_resolution/schema +-- requires: schemas/function_resolution/procedures/frame_candidates + +BEGIN; + +-- resolve_bucket: answer "which bucket serves this selector for this +-- execution", from the published buckets catalog. +-- +-- The selector is {tags, type}: tag containment plus an optional access-type +-- filter. That is the whole vocabulary, on purpose — a function definition is +-- tenant-agnostic (one image serves many databases, so it cannot name a bucket +-- id) and a bucket's key is owner-local identity a tenant names whatever it +-- likes, so meaning lives in labels the tenant applies. Conventions like +-- uploads / variants / exports are documentation, not DDL: nothing in the +-- schema constrains a tenant to one bucket per tag. +-- +-- Determinism is enforced here rather than by a unique index: candidates are +-- probed most-specific frame first, and the winning frame must answer with +-- exactly one bucket. Zero matches and several matches both raise, and the +-- ambiguous error names the candidates so the fix (retag, narrow by type, or +-- write an explicit capability binding) is obvious. +-- +-- Cross-scope reach follows the catalog's own visibility rule: a bucket owned +-- by another database resolves only when is_visible (propagated from the +-- source row's is_public), so an outer frame's private bucket is unreachable. +-- +-- One static set-based query answers every frame at once. The shared plane holds +-- every logical database's rows, so each candidate carries the row's expected +-- database_id — the candidate's own key at database scope, the frame's lookup +-- database otherwise — exactly as resolve() does for functions. Without it one +-- tenant's probe could be answered by another tenant's row. +-- +-- A frame database without a buckets catalog simply contributes no candidates: +-- storage is an optional module, so its absence is not a provisioning error the +-- way a missing functions catalog is. +CREATE FUNCTION function_resolution.resolve_bucket( + database_id uuid, + scope text, + entity_id uuid, + tags text[], + type_filter text DEFAULT NULL +) RETURNS TABLE ( + bucket_id uuid, + bucket_key text, + bucket_type text, + physical_name text, + owner_database_id uuid, + owner_scope text, + owner_key uuid +) AS $$ +DECLARE + v_matches jsonb; + v_match jsonb; +BEGIN + IF resolve_bucket.tags IS NULL OR cardinality(resolve_bucket.tags) = 0 THEN + RAISE EXCEPTION 'CAPABILITY_BUCKET_SELECTOR_EMPTY: a bucket selector needs at least one tag (database_id=%, scope="%")', + resolve_bucket.database_id, resolve_bucket.scope + USING ERRCODE = 'FR010'; + END IF; + + -- Every frame in one indexed read, keeping only the matches of the most + -- specific frame that answered: a nearer frame outranks an outer one, and + -- ties within that frame are the ambiguity raised below. + WITH hits AS ( + SELECT b.id, + b.key, + b.type, + b.physical_name, + b.database_id, + b.owner_scope, + b.owner_key, + cand.ord + FROM function_resolution.frame_candidates( + resolve_bucket.database_id, + resolve_bucket.scope, + resolve_bucket.entity_id + ) cand + JOIN catalog_private.buckets b + ON b.owner_scope = cand.owner_scope + AND b.owner_key IS NOT DISTINCT FROM cand.owner_key + AND b.database_id = CASE + WHEN cand.owner_scope = 'database' THEN cand.owner_key + ELSE cand.lookup_database_id + END + WHERE b.tags @> resolve_bucket.tags + AND (resolve_bucket.type_filter IS NULL OR b.type = resolve_bucket.type_filter) + AND (b.database_id = resolve_bucket.database_id OR b.is_visible) + ), + nearest AS ( + SELECT h.* + FROM hits h + WHERE h.ord = (SELECT min(hh.ord) FROM hits hh) + ) + SELECT COALESCE(jsonb_agg(to_jsonb(n) ORDER BY n.id), '[]'::jsonb) + INTO v_matches + FROM nearest n; + + IF jsonb_array_length(v_matches) = 0 THEN + RAISE EXCEPTION 'CAPABILITY_BUCKET_NOT_FOUND: no bucket tagged % % resolves in the scope chain starting at scope "%" (database_id=%)', + resolve_bucket.tags, + COALESCE('of type ' || resolve_bucket.type_filter, '(any type)'), + resolve_bucket.scope, + resolve_bucket.database_id + USING ERRCODE = 'FR011'; + END IF; + + IF jsonb_array_length(v_matches) > 1 THEN + RAISE EXCEPTION 'CAPABILITY_BUCKET_AMBIGUOUS: % buckets tagged % % resolve equally (candidates: %); retag, narrow by type, or bind the capability explicitly', + jsonb_array_length(v_matches), + resolve_bucket.tags, + COALESCE('of type ' || resolve_bucket.type_filter, '(any type)'), + (SELECT string_agg(format('%s (%s)', m->>'key', m->>'id'), ', ' ORDER BY m->>'key') + FROM jsonb_array_elements(v_matches) m) + USING ERRCODE = 'FR012'; + END IF; + + v_match := v_matches->0; + + resolve_bucket.bucket_id := (v_match->>'id')::uuid; + resolve_bucket.bucket_key := v_match->>'key'; + resolve_bucket.bucket_type := v_match->>'type'; + resolve_bucket.physical_name := v_match->>'physical_name'; + resolve_bucket.owner_database_id := (v_match->>'database_id')::uuid; + resolve_bucket.owner_scope := v_match->>'owner_scope'; + resolve_bucket.owner_key := (v_match->>'owner_key')::uuid; + + RETURN NEXT; +END; +$$ LANGUAGE plpgsql STABLE SECURITY DEFINER; + +COMMIT; diff --git a/packages/function-resolution/deploy/schemas/function_resolution/procedures/resolve_capabilities.sql b/packages/function-resolution/deploy/schemas/function_resolution/procedures/resolve_capabilities.sql new file mode 100644 index 000000000..813dc9112 --- /dev/null +++ b/packages/function-resolution/deploy/schemas/function_resolution/procedures/resolve_capabilities.sql @@ -0,0 +1,278 @@ +-- Deploy schemas/function_resolution/procedures/resolve_capabilities to pg + +-- requires: schemas/function_resolution/schema +-- requires: schemas/function_resolution/procedures/definitions_location +-- requires: schemas/function_resolution/procedures/frame_candidates +-- requires: schemas/function_resolution/procedures/resolve_bucket +-- requires: schemas/function_resolution/procedures/bucket_catalog_row +-- requires: schemas/function_resolution/procedures/bound_bucket_id +-- requires: schemas/function_resolution/procedures/resolve_api +-- requires: schemas/function_resolution/procedures/resolve_payload_refs + +BEGIN; + +-- resolve_capabilities: turn one resolved function definition plus one raw +-- payload into the bundle an invocation runs with — resolve-before-dispatch. +-- +-- This is the single implementation of capability resolution, and it must stay +-- single: the worker calls it before invoking a function, and a graph node that +-- resolves capabilities for downstream nodes is a wrapper over this same +-- function. Two implementations would be two answers to "which bucket is +-- this". +-- +-- A definition declares tenant-agnostically (required_buckets keys, +-- required_modules selectors); a tenant fulfils either by labelling its own +-- rows (discovery) or by writing a capability binding (the deterministic +-- override). +-- +-- Where the definition lives and where it runs are separate: a platform-scope +-- definition is invoked inside a tenant's (or an org's, or a department's) +-- frame chain and RLS world, which is why the execution triple +-- (database_id, scope, entity_id) is taken apart from the definition's +-- (definition_scope, definition_database_id). Every requirement resolves +-- against the *execution's* frames, so one image serves every scope. +-- Every capability therefore has exactly one answer here, or the invocation +-- fails loudly before any code runs — a function never receives a half-built +-- context, and never selects a resource itself. +-- +-- Buckets resolve in three tiers, and only the third one reaches this +-- declaration path: +-- 1. record-associated resources are stamped into the payload when the +-- trigger is created (the $ref is already resolved: passed through), +-- 2. a file field's bucket is a metaschema fact, queried where it is used, +-- 3. function-owned resources (scratch, exports, variants) are declared in +-- required_buckets and resolved here. +-- +-- The returned bundle carries coordinates and handles only. Secret and config +-- requirements come back as the names the definition declared — never values: +-- those resolve per invocation through the realm's getter inside the runtime, +-- so a credential never enters a payload, a bundle, or a queue row. +CREATE FUNCTION function_resolution.resolve_capabilities( + database_id uuid, + scope text, + entity_id uuid, + function_definition_id uuid, + definition_scope text, + definition_database_id uuid DEFAULT NULL, + payload jsonb DEFAULT '{}'::jsonb, + channel text DEFAULT NULL +) RETURNS jsonb AS $$ +DECLARE + v_defn_database_id uuid; + v_defs_schema text; + v_defs_table text; + v_query text; + v_definition jsonb; + v_access_channels text[]; + v_key text; + v_bound_bucket_id uuid; + -- The keys a tenant fulfilled with an explicit binding, paired positionally + -- with the bucket each binding names: the two resolution routes are disjoint + -- sets of keys, resolved by one query each rather than key by key. + v_bound_keys text[]; + v_bound_ids uuid[]; + v_buckets jsonb := '{}'::jsonb; + v_apis jsonb := '{}'::jsonb; +BEGIN + v_defn_database_id := coalesce( + resolve_capabilities.definition_database_id, + resolve_capabilities.database_id + ); + + SELECT l.schema_name, l.table_name + INTO v_defs_schema, v_defs_table + FROM function_resolution.definitions_location(v_defn_database_id, resolve_capabilities.definition_scope) l; + + IF v_defs_schema IS NULL THEN + RAISE EXCEPTION 'CAPABILITY_DEFINITION_SCOPE_UNPROVISIONED: database % has no function module at scope "%"', + v_defn_database_id, resolve_capabilities.definition_scope + USING ERRCODE = 'FR040'; + END IF; + + -- to_jsonb of the row rather than a column list: the declaration set grows, + -- and a resolver that names columns fails on a database whose function + -- module predates the newest one. + -- pgsql-lint-disable-next-line no-dynamic-sql -- lookup-only: the definitions table is located per scope + v_query := format('SELECT to_jsonb(d) FROM %I.%I d WHERE d.id = $1', v_defs_schema, v_defs_table); + + EXECUTE v_query INTO v_definition USING resolve_capabilities.function_definition_id; + + IF v_definition IS NULL THEN + RAISE EXCEPTION 'CAPABILITY_DEFINITION_NOT_FOUND: no function definition % at scope "%" in database %', + resolve_capabilities.function_definition_id, + resolve_capabilities.definition_scope, + v_defn_database_id + USING ERRCODE = 'FR040'; + END IF; + + -- Lane check: access_channels is who may invoke a function, so an + -- invocation arriving through a channel the definition does not list is + -- refused here rather than at the image, which cannot know. + IF resolve_capabilities.channel IS NOT NULL THEN + SELECT array_agg(c.channel) + INTO v_access_channels + FROM jsonb_array_elements_text(coalesce(v_definition->'access_channels', '[]'::jsonb)) AS c(channel); + + IF NOT coalesce(v_access_channels, ARRAY[]::text[]) @> ARRAY[resolve_capabilities.channel] THEN + RAISE EXCEPTION 'CAPABILITY_CHANNEL_REFUSED: function % does not declare the "%" access channel (declares: %)', + resolve_capabilities.function_definition_id, + resolve_capabilities.channel, + coalesce(array_to_string(v_access_channels, ', '), '') + USING ERRCODE = 'FR041'; + END IF; + END IF; + + -- ========================================================================= + -- required_buckets: explicit binding first, then discovery by tag + -- + -- Set-based, in three statements rather than a loop per key, because every + -- declared key resolves independently: the bindings are read once, the + -- reachability of all of them is proved once, and the remaining keys are + -- resolved by tag once. The two routes are kept in separate statements on + -- purpose — resolve_bucket RAISES when a tag matches nothing, so evaluating + -- it for a bound key (which needs no tag) would turn a valid declaration + -- into an error, and a LEFT JOIN LATERAL's ON clause is no guarantee the + -- function is not evaluated. + -- ========================================================================= + SELECT array_agg(b.key ORDER BY b.ord), array_agg(b.bucket_id ORDER BY b.ord) + INTO v_bound_keys, v_bound_ids + FROM ( + SELECT k.key, + k.ord, + function_resolution.bound_bucket_id( + resolve_capabilities.database_id, + resolve_capabilities.scope, + resolve_capabilities.entity_id, + resolve_capabilities.function_definition_id, + k.key + ) AS bucket_id + FROM jsonb_array_elements_text( + coalesce(v_definition->'required_buckets', '[]'::jsonb) + ) WITH ORDINALITY AS k(key, ord) + ) b + WHERE b.bucket_id IS NOT NULL; + + -- Same-tenant enforcement: a binding naming a bucket outside the execution's + -- own frame chain (or an outer frame's private one) must fail the whole + -- invocation. The generated binding guard cannot check this — compute's + -- published modules may not reference storage — so it is checked here, where + -- a function would otherwise be handed the bucket. + IF v_bound_keys IS NOT NULL THEN + SELECT b.key, b.bucket_id + INTO v_key, v_bound_bucket_id + FROM unnest(v_bound_keys, v_bound_ids) AS b(key, bucket_id) + WHERE NOT EXISTS ( + SELECT 1 + FROM function_resolution.bucket_catalog_row( + resolve_capabilities.database_id, + resolve_capabilities.scope, + resolve_capabilities.entity_id, + b.bucket_id + ) + ) + ORDER BY b.key + LIMIT 1; + + IF FOUND THEN + RAISE EXCEPTION 'CAPABILITY_BINDING_UNREACHABLE: capability "%" of function % is bound to bucket %, which database % may not reach', + v_key, + resolve_capabilities.function_definition_id, + v_bound_bucket_id, + resolve_capabilities.database_id + USING ERRCODE = 'FR013'; + END IF; + + SELECT jsonb_object_agg(b.key, jsonb_build_object( + 'bucket_id', b.bucket_id, + 'key', c.bucket_key, + 'type', c.bucket_type, + 'physical_name', c.physical_name, + 'database_id', c.owner_database_id, + 'source', 'binding' + )) + INTO v_buckets + FROM unnest(v_bound_keys, v_bound_ids) AS b(key, bucket_id) + CROSS JOIN LATERAL function_resolution.bucket_catalog_row( + resolve_capabilities.database_id, + resolve_capabilities.scope, + resolve_capabilities.entity_id, + b.bucket_id + ) c; + END IF; + + -- Discovery by tag, for every declared key the tenant did not bind. The + -- unbound set is a MATERIALIZED CTE so the bound keys are excluded *before* + -- resolve_bucket runs: a bound key needs no tag, and evaluating it would + -- raise CAPABILITY_BUCKET_NOT_FOUND on a perfectly valid declaration. + WITH unbound AS MATERIALIZED ( + SELECT k.key + FROM jsonb_array_elements_text( + coalesce(v_definition->'required_buckets', '[]'::jsonb) + ) AS k(key) + WHERE NOT k.key = ANY(coalesce(v_bound_keys, ARRAY[]::text[])) + ) + SELECT coalesce(v_buckets, '{}'::jsonb) || coalesce(jsonb_object_agg(k.key, jsonb_build_object( + 'bucket_id', r.bucket_id, + 'key', r.bucket_key, + 'type', r.bucket_type, + 'physical_name', r.physical_name, + 'database_id', r.owner_database_id, + 'source', 'tags' + )), '{}'::jsonb) + INTO v_buckets + FROM unbound k + CROSS JOIN LATERAL function_resolution.resolve_bucket( + resolve_capabilities.database_id, + resolve_capabilities.scope, + resolve_capabilities.entity_id, + ARRAY[k.key], + NULL + ) r; + + -- ========================================================================= + -- required_modules: module names, the same vocabulary the presets use + -- ([.][@], or a bare api name as the escape hatch). + -- resolve_api raises on an unresolvable selector, which propagates out of + -- the lateral and fails the invocation — the intended behaviour. + -- ========================================================================= + SELECT coalesce(jsonb_object_agg(s.selector, jsonb_build_object( + 'api_id', a.api_id, + 'name', a.api_name, + 'database_id', a.owner_database_id + )), '{}'::jsonb) + INTO v_apis + FROM jsonb_array_elements_text( + coalesce(v_definition->'required_modules', '[]'::jsonb) + ) AS s(selector) + CROSS JOIN LATERAL function_resolution.resolve_api( + resolve_capabilities.database_id, + resolve_capabilities.scope, + resolve_capabilities.entity_id, + s.selector + ) a; + + RETURN jsonb_build_object( + 'function_definition_id', resolve_capabilities.function_definition_id, + 'definition_scope', resolve_capabilities.definition_scope, + 'definition_database_id', v_defn_database_id, + 'database_id', resolve_capabilities.database_id, + 'scope', resolve_capabilities.scope, + 'entity_id', resolve_capabilities.entity_id, + 'buckets', v_buckets, + 'apis', v_apis, + 'models', coalesce(v_definition->'required_models', '[]'::jsonb), + 'secrets', coalesce(v_definition->'required_secrets', '[]'::jsonb), + 'configs', coalesce(v_definition->'required_configs', '[]'::jsonb), + 'integrations', coalesce(v_definition->'integrations', '[]'::jsonb), + 'access_channels', coalesce(v_definition->'access_channels', '[]'::jsonb), + 'payload', function_resolution.resolve_payload_refs( + resolve_capabilities.database_id, + resolve_capabilities.scope, + resolve_capabilities.entity_id, + coalesce(resolve_capabilities.payload, '{}'::jsonb) + ) + ); +END; +$$ LANGUAGE plpgsql STABLE SECURITY DEFINER; + +COMMIT; diff --git a/packages/function-resolution/deploy/schemas/function_resolution/procedures/resolve_invocation.sql b/packages/function-resolution/deploy/schemas/function_resolution/procedures/resolve_invocation.sql index 4b14ce881..f67914bf2 100644 --- a/packages/function-resolution/deploy/schemas/function_resolution/procedures/resolve_invocation.sql +++ b/packages/function-resolution/deploy/schemas/function_resolution/procedures/resolve_invocation.sql @@ -1,7 +1,7 @@ -- Deploy schemas/function_resolution/procedures/resolve_invocation to pg -- requires: schemas/function_resolution/schema -- requires: schemas/function_resolution/procedures/resolve --- requires: schemas/function_resolution/procedures/catalog_location +-- requires: metaschema-modules:schemas/metaschema_modules_public/tables/catalog_module/table -- requires: pgpm-app-scope:schemas/app_scope/procedures/frames BEGIN; @@ -16,7 +16,11 @@ BEGIN; -- typed functions catalog of the declared scope's frame database. The -- scope's lookup database + key come from app_scope.frames, so validation -- keys exactly like resolution (e.g. an `org` pair keys by the owning org, --- not the database). A frame database without a functions catalog raises +-- not the database). Validation is static SQL against +-- catalog_private.functions, keyed by the same database_id the catalog-sync +-- triggers stamp (the scope key at database scope, the writing database +-- otherwise), so one tenant's pair can never be validated against another's +-- row. A frame database without a functions catalog raises -- FUNCTION_RESOLUTION_CATALOG_UNAVAILABLE (SQLSTATE FR001). -- * otherwise: resolve deterministically across scopes. Definition-less -- invocations stay allowed (require_definition => false), leaving both @@ -38,8 +42,7 @@ DECLARE v_lookup_db uuid; v_probe_key uuid; v_found uuid; - v_catalog_schema text; - v_catalog_table text; + v_expected_db uuid; BEGIN -- API-provenance path (api_binding_id present) must declare its definition -- explicitly — the function_callable_check policy verifies the binding @@ -67,27 +70,42 @@ BEGIN v_lookup_db := database_id; END IF; - SELECT l.schema_name, l.table_name - INTO v_catalog_schema, v_catalog_table - FROM function_resolution.catalog_location(v_lookup_db) l; - - IF v_catalog_schema IS NULL THEN + IF NOT EXISTS ( + SELECT 1 + FROM metaschema_modules_public.catalog_module cm + WHERE cm.database_id = v_lookup_db + AND cm.functions_table_id IS NOT NULL + AND cm.functions_table_id <> uuid_nil() + ) THEN RAISE EXCEPTION 'FUNCTION_RESOLUTION_CATALOG_UNAVAILABLE: database % has no functions catalog to validate pair against (task_identifier "%")', v_lookup_db, task_identifier USING ERRCODE = 'FR001'; END IF; - -- Same two-pass keying as resolution: the exact scope-key row wins, - -- the scope-default (owner_key IS NULL) row only as fallback. - EXECUTE format( - 'SELECT id FROM %I.%I WHERE owner_scope = $1 AND task_identifier = $2 AND owner_key = $3', - v_catalog_schema, v_catalog_table - ) INTO v_found USING v_scope, task_identifier, v_probe_key; + -- Same two-pass keying as resolution: the exact scope-key row wins, the + -- scope-default (owner_key IS NULL) row only as fallback. Both carry the + -- shared plane's row-identity predicate. + v_expected_db := CASE + WHEN v_scope = 'database' AND v_probe_key IS NOT NULL THEN v_probe_key + ELSE v_lookup_db + END; + + SELECT c.id + INTO v_found + FROM catalog_private.functions c + WHERE c.owner_scope = v_scope + AND c.task_identifier = resolve_invocation.task_identifier + AND c.owner_key = v_probe_key + AND c.database_id = v_expected_db; + IF v_found IS NULL THEN - EXECUTE format( - 'SELECT id FROM %I.%I WHERE owner_scope = $1 AND task_identifier = $2 AND owner_key IS NULL', - v_catalog_schema, v_catalog_table - ) INTO v_found USING v_scope, task_identifier; + SELECT c.id + INTO v_found + FROM catalog_private.functions c + WHERE c.owner_scope = v_scope + AND c.task_identifier = resolve_invocation.task_identifier + AND c.owner_key IS NULL + AND c.database_id = v_lookup_db; END IF; IF v_found IS DISTINCT FROM existing_id THEN diff --git a/packages/function-resolution/deploy/schemas/function_resolution/procedures/resolve_payload_refs.sql b/packages/function-resolution/deploy/schemas/function_resolution/procedures/resolve_payload_refs.sql new file mode 100644 index 000000000..648e4e47a --- /dev/null +++ b/packages/function-resolution/deploy/schemas/function_resolution/procedures/resolve_payload_refs.sql @@ -0,0 +1,205 @@ +-- Deploy schemas/function_resolution/procedures/resolve_payload_refs to pg + +-- requires: schemas/function_resolution/schema +-- requires: schemas/function_resolution/procedures/resolve_bucket +-- requires: schemas/function_resolution/procedures/resolve_api +-- requires: metaschema-schema:schemas/metaschema_public/tables/table/table + +BEGIN; + +-- resolve_payload_refs: rewrite a job payload's typed references into +-- actionable coordinates, before anything outside the database sees it. +-- +-- A payload is otherwise opaque jsonb: nothing tells a worker which string is a +-- bucket and which is a label. Tagging the leaf makes it self-describing, and +-- one pass turns the whole payload into data a function can act on without ever +-- querying the metaschema — which is the property that lets a function image be +-- tenant-agnostic and language-agnostic at once: +-- +-- {"$ref": "bucket", "tags": ["variants"], "type": "private"} +-- {"$ref": "table", "schema": "public", "name": "documents"} +-- {"$ref": "api", "module": "notifications_module"} -- or {"name": "admin"} +-- +-- Secrets are deliberately NOT part of this vocabulary. A payload carries +-- coordinates and handles, never a credential (nor even a credential's name to +-- be looked up later); required_secrets on the definition stays the only +-- declaration, resolved per invocation through the realm's getter inside the +-- runtime. +-- +-- An unknown $ref kind raises rather than passing through, so a typo in a node +-- type's payload schema fails at the boundary instead of reaching a function as +-- an object it will silently ignore. +-- +-- Idempotent: a node that already carries its resolved id is left alone, so +-- re-running the pass over an already-resolved payload is a no-op. +CREATE FUNCTION function_resolution.resolve_payload_refs( + database_id uuid, + scope text, + entity_id uuid, + payload jsonb +) RETURNS jsonb AS $$ +DECLARE + v_kind text; + v_tags text[]; + v_bucket record; + v_api record; + v_selector text; + v_schema text; + v_name text; + v_table_id uuid; + v_result jsonb; + v_key text; +BEGIN + IF resolve_payload_refs.payload IS NULL THEN + RETURN NULL; + END IF; + + IF jsonb_typeof(resolve_payload_refs.payload) = 'array' THEN + SELECT coalesce(jsonb_agg( + function_resolution.resolve_payload_refs( + resolve_payload_refs.database_id, + resolve_payload_refs.scope, + resolve_payload_refs.entity_id, + elem + ) ORDER BY ord + ), '[]'::jsonb) + INTO v_result + FROM jsonb_array_elements(resolve_payload_refs.payload) WITH ORDINALITY AS t(elem, ord); + + RETURN v_result; + END IF; + + IF jsonb_typeof(resolve_payload_refs.payload) <> 'object' THEN + RETURN resolve_payload_refs.payload; + END IF; + + IF NOT (resolve_payload_refs.payload ? '$ref') THEN + v_result := '{}'::jsonb; + + FOR v_key IN SELECT k FROM jsonb_object_keys(resolve_payload_refs.payload) k + LOOP + v_result := v_result || jsonb_build_object( + v_key, + function_resolution.resolve_payload_refs( + resolve_payload_refs.database_id, + resolve_payload_refs.scope, + resolve_payload_refs.entity_id, + resolve_payload_refs.payload -> v_key + ) + ); + END LOOP; + + RETURN v_result; + END IF; + + v_kind := resolve_payload_refs.payload->>'$ref'; + + IF v_kind = 'bucket' THEN + -- Already resolved (trigger-time stamping, or a second pass). + IF resolve_payload_refs.payload ? 'bucket_id' THEN + RETURN resolve_payload_refs.payload; + END IF; + + IF resolve_payload_refs.payload ? 'tags' THEN + SELECT array_agg(t.tag ORDER BY t.ord) + INTO v_tags + FROM jsonb_array_elements_text(resolve_payload_refs.payload->'tags') + WITH ORDINALITY AS t(tag, ord); + ELSIF resolve_payload_refs.payload ? 'key' THEN + -- A single key is the one-tag selector: the declaration vocabulary + -- and the payload vocabulary stay the same thing. + v_tags := ARRAY[resolve_payload_refs.payload->>'key']; + END IF; + + SELECT * + INTO v_bucket + FROM function_resolution.resolve_bucket( + resolve_payload_refs.database_id, + resolve_payload_refs.scope, + resolve_payload_refs.entity_id, + v_tags, + resolve_payload_refs.payload->>'type' + ); + + RETURN jsonb_build_object( + '$ref', 'bucket', + 'bucket_id', v_bucket.bucket_id, + 'key', v_bucket.bucket_key, + 'type', v_bucket.bucket_type, + 'physical_name', v_bucket.physical_name, + 'database_id', v_bucket.owner_database_id + ); + END IF; + + IF v_kind = 'api' THEN + IF resolve_payload_refs.payload ? 'api_id' THEN + RETURN resolve_payload_refs.payload; + END IF; + + -- Both keys carry the selector vocabulary itself: module is a module + -- name (optionally .api / @scope), name is an api name. + v_selector := coalesce( + resolve_payload_refs.payload->>'module', + resolve_payload_refs.payload->>'name' + ); + + SELECT * + INTO v_api + FROM function_resolution.resolve_api( + resolve_payload_refs.database_id, + resolve_payload_refs.scope, + resolve_payload_refs.entity_id, + v_selector + ); + + RETURN jsonb_build_object( + '$ref', 'api', + 'api_id', v_api.api_id, + 'name', v_api.api_name, + 'database_id', v_api.owner_database_id + ); + END IF; + + IF v_kind = 'table' THEN + IF resolve_payload_refs.payload ? 'table_id' THEN + RETURN resolve_payload_refs.payload; + END IF; + + v_schema := resolve_payload_refs.payload->>'schema'; + v_name := resolve_payload_refs.payload->>'name'; + + IF v_schema IS NULL OR v_name IS NULL THEN + RAISE EXCEPTION 'CAPABILITY_TABLE_REF_INVALID: a table reference needs both schema and name (got %)', + resolve_payload_refs.payload + USING ERRCODE = 'FR031'; + END IF; + + SELECT t.id + INTO v_table_id + FROM metaschema_public.schema s + JOIN metaschema_public."table" t ON (t.schema_id = s.id AND t.database_id = s.database_id) + WHERE s.database_id = resolve_payload_refs.database_id + AND s.schema_name = v_schema + AND t.name = v_name; + + IF NOT FOUND THEN + RAISE EXCEPTION 'CAPABILITY_TABLE_NOT_FOUND: database % has no table %.%', + resolve_payload_refs.database_id, v_schema, v_name + USING ERRCODE = 'FR032'; + END IF; + + RETURN jsonb_build_object( + '$ref', 'table', + 'schema', v_schema, + 'name', v_name, + 'table_id', v_table_id + ); + END IF; + + RAISE EXCEPTION 'CAPABILITY_REF_UNKNOWN: "%" is not a known payload reference kind (known kinds: bucket, table, api)', + v_kind + USING ERRCODE = 'FR030'; +END; +$$ LANGUAGE plpgsql STABLE SECURITY DEFINER; + +COMMIT; diff --git a/packages/function-resolution/deploy/schemas/function_resolution/procedures/routing.sql b/packages/function-resolution/deploy/schemas/function_resolution/procedures/routing.sql index 28a023e86..dabc5218f 100644 --- a/packages/function-resolution/deploy/schemas/function_resolution/procedures/routing.sql +++ b/packages/function-resolution/deploy/schemas/function_resolution/procedures/routing.sql @@ -1,56 +1,40 @@ -- Deploy schemas/function_resolution/procedures/routing to pg -- requires: schemas/function_resolution/schema --- requires: schemas/function_resolution/procedures/definitions_location BEGIN; -- routing: load the queue-routing fields (queue_name, priority, max_attempts) -- from ONE already-resolved function definition. -- --- The winning definition's scope is already known, so this does NOT re-walk the --- scope chain — it loads the single row by id from that scope's definitions --- table (located deterministically via function_resolution.definitions_location). --- Callers pass the DEFINITION's home database (the platform database for a --- platform-scope definition, the execution database otherwise). Returns no row --- when the scope has no provisioned function_module or the id is absent, letting --- the caller fall back to queue defaults. +-- The catalog row carries the routing fields (copied from the source definition +-- by the catalog-sync triggers) and its id IS the source definition's id, so +-- this is one primary-key read of catalog_private.functions: resolution and +-- routing answer from the same row, and the scoped source definitions table is +-- never touched on the dispatch path. -- --- The probe is a dynamic SELECT against a dynamically-named table, built with --- format()/quote_ident + EXECUTE ... USING. No AST/deparser dependency. +-- The database_id predicate is what keeps one tenant from reading another's row +-- off the shared plane, even if an id were guessed. Returns no row when the id +-- is absent, letting the caller fall back to queue defaults. CREATE FUNCTION function_resolution.routing( database_id uuid, - scope text, function_definition_id uuid ) RETURNS TABLE ( queue_name text, priority integer, max_attempts integer ) AS $$ -DECLARE - v_schema text; - v_table text; - v_entity_field text; - v_query text; BEGIN - SELECT l.schema_name, l.table_name, l.entity_field - INTO v_schema, v_table, v_entity_field - FROM function_resolution.definitions_location(routing.database_id, routing.scope) l; - - IF v_schema IS NULL OR function_definition_id IS NULL THEN + IF function_definition_id IS NULL THEN RETURN; END IF; - -- SELECT queue_name, priority, max_attempts FROM ""."" WHERE id = $1 - v_query := format( - 'SELECT queue_name, priority, max_attempts FROM %I.%I WHERE id = $1', - v_schema, v_table - ); - - EXECUTE v_query - INTO routing.queue_name, - routing.priority, - routing.max_attempts - USING function_definition_id; + SELECT c.queue_name, c.priority, c.max_attempts + INTO routing.queue_name, + routing.priority, + routing.max_attempts + FROM catalog_private.functions c + WHERE c.id = routing.function_definition_id + AND c.database_id = routing.database_id; -- Only surface a routing row when the definition actually exists (a missing -- id leaves every column NULL — the caller wants defaults, not a NULL row). diff --git a/packages/function-resolution/deploy/schemas/function_resolution/procedures/validate_capabilities.sql b/packages/function-resolution/deploy/schemas/function_resolution/procedures/validate_capabilities.sql new file mode 100644 index 000000000..b6b97c96a --- /dev/null +++ b/packages/function-resolution/deploy/schemas/function_resolution/procedures/validate_capabilities.sql @@ -0,0 +1,47 @@ +-- Deploy schemas/function_resolution/procedures/validate_capabilities to pg + +-- requires: schemas/function_resolution/schema +-- requires: schemas/function_resolution/procedures/resolve_capabilities + +BEGIN; + +-- validate_capabilities: prove an invocation's declarations can be satisfied, +-- without keeping the bundle. +-- +-- Deliberately a thin wrapper over resolve_capabilities rather than a parallel +-- set of checks: validation that does not run the real resolution eventually +-- disagrees with it, and then a deploy-time check passes while dispatch fails. +-- Every requirement kind is therefore validated by resolving it — buckets +-- (declared key -> binding or tags), apis (semantic selector), the payload's +-- typed refs, the channel/lane, and the reachability of every bound target. +-- +-- Secret, config, model and integration requirements are returned by resolution +-- as declared names and are not resolved here: their values live per realm and +-- are fetched inside the runtime, and integration requirements are copied into +-- the definition at authoring time on purpose — a later edit to a provider row +-- must not silently change what an already-deployed function requires. +CREATE FUNCTION function_resolution.validate_capabilities( + database_id uuid, + scope text, + entity_id uuid, + function_definition_id uuid, + definition_scope text, + definition_database_id uuid DEFAULT NULL, + payload jsonb DEFAULT '{}'::jsonb, + channel text DEFAULT NULL +) RETURNS void AS $$ +BEGIN + PERFORM function_resolution.resolve_capabilities( + validate_capabilities.database_id, + validate_capabilities.scope, + validate_capabilities.entity_id, + validate_capabilities.function_definition_id, + validate_capabilities.definition_scope, + validate_capabilities.definition_database_id, + validate_capabilities.payload, + validate_capabilities.channel + ); +END; +$$ LANGUAGE plpgsql STABLE SECURITY DEFINER; + +COMMIT; diff --git a/packages/function-resolution/package.json b/packages/function-resolution/package.json index 9d84f02dd..fd23019bf 100644 --- a/packages/function-resolution/package.json +++ b/packages/function-resolution/package.json @@ -1,6 +1,6 @@ { "name": "@pgpm/function-resolution", - "version": "0.36.0", + "version": "0.39.0", "description": "Portable cross-scope function-definition resolution and resolver-aware enqueue (built on app-scope)", "author": "Dan Lynch ", "contributors": [ diff --git a/packages/function-resolution/pgpm-function-resolution.control b/packages/function-resolution/pgpm-function-resolution.control index 79b750842..78023abae 100644 --- a/packages/function-resolution/pgpm-function-resolution.control +++ b/packages/function-resolution/pgpm-function-resolution.control @@ -1,6 +1,6 @@ # pgpm-function-resolution extension comment = 'pgpm-function-resolution extension' -default_version = '0.36.0' +default_version = '0.39.0' module_pathname = '$libdir/pgpm-function-resolution' requires = 'plpgsql,pgpm-verify,metaschema-schema,metaschema-modules,pgpm-app-scope,pgpm-database-jobs,pgpm-jwt-claims' relocatable = false diff --git a/packages/function-resolution/pgpm.plan b/packages/function-resolution/pgpm.plan index 2ec40c124..b98f1fd84 100644 --- a/packages/function-resolution/pgpm.plan +++ b/packages/function-resolution/pgpm.plan @@ -4,8 +4,16 @@ schemas/function_resolution/schema 2017-08-11T08:11:51Z constructive # function_resolution schema schemas/function_resolution/procedures/definitions_location [schemas/function_resolution/schema] 2017-08-11T08:11:51Z constructive # locate a scope defs table -schemas/function_resolution/procedures/routing [schemas/function_resolution/schema schemas/function_resolution/procedures/definitions_location] 2017-08-11T08:11:51Z constructive # queue routing for a definition -schemas/function_resolution/procedures/catalog_location [schemas/function_resolution/schema] 2017-08-11T08:11:51Z constructive # locate a database's functions catalog -schemas/function_resolution/procedures/resolve [schemas/function_resolution/schema schemas/function_resolution/procedures/catalog_location] 2017-08-11T08:11:51Z constructive # cross-scope resolver -schemas/function_resolution/procedures/resolve_invocation [schemas/function_resolution/schema schemas/function_resolution/procedures/resolve schemas/function_resolution/procedures/catalog_location] 2017-08-11T08:11:51Z constructive # invocation-lane resolver +schemas/function_resolution/procedures/routing [schemas/function_resolution/schema] 2017-08-11T08:11:51Z constructive # queue routing for a definition +schemas/function_resolution/procedures/resolve [schemas/function_resolution/schema] 2017-08-11T08:11:51Z constructive # cross-scope resolver +schemas/function_resolution/procedures/resolve_invocation [schemas/function_resolution/schema schemas/function_resolution/procedures/resolve] 2017-08-11T08:11:51Z constructive # invocation-lane resolver schemas/function_resolution/procedures/enqueue [schemas/function_resolution/schema schemas/function_resolution/procedures/routing schemas/function_resolution/procedures/resolve] 2017-08-11T08:11:51Z constructive # resolver-aware enqueue entry point +schemas/function_resolution/procedures/frame_candidates [schemas/function_resolution/schema] 2017-08-11T08:11:51Z constructive # frames expanded into catalog probe candidates +schemas/function_resolution/procedures/resolve_bucket [schemas/function_resolution/schema schemas/function_resolution/procedures/frame_candidates] 2017-08-11T08:11:51Z constructive # bucket selector {tags,type} resolution +schemas/function_resolution/procedures/bucket_catalog_row [schemas/function_resolution/schema schemas/function_resolution/procedures/frame_candidates] 2017-08-11T08:11:51Z constructive # reachable bucket by id (same-tenant proof) +schemas/function_resolution/procedures/api_catalog_row [schemas/function_resolution/schema schemas/function_resolution/procedures/frame_candidates] 2017-08-11T08:11:51Z constructive # reachable api by id +schemas/function_resolution/procedures/resolve_api [schemas/function_resolution/schema schemas/function_resolution/procedures/frame_candidates schemas/function_resolution/procedures/api_catalog_row] 2017-08-11T08:11:51Z constructive # api selector module:/name: resolution +schemas/function_resolution/procedures/bound_bucket_id [schemas/function_resolution/schema schemas/function_resolution/procedures/frame_candidates] 2017-08-11T08:11:51Z constructive # explicit capability binding override +schemas/function_resolution/procedures/resolve_payload_refs [schemas/function_resolution/schema schemas/function_resolution/procedures/resolve_bucket schemas/function_resolution/procedures/resolve_api] 2017-08-11T08:11:51Z constructive # tagged payload refs to coordinates +schemas/function_resolution/procedures/resolve_capabilities [schemas/function_resolution/schema schemas/function_resolution/procedures/definitions_location schemas/function_resolution/procedures/frame_candidates schemas/function_resolution/procedures/resolve_bucket schemas/function_resolution/procedures/bucket_catalog_row schemas/function_resolution/procedures/bound_bucket_id schemas/function_resolution/procedures/resolve_api schemas/function_resolution/procedures/resolve_payload_refs] 2017-08-11T08:11:51Z constructive # resolve-before-dispatch capability bundle +schemas/function_resolution/procedures/validate_capabilities [schemas/function_resolution/schema schemas/function_resolution/procedures/resolve_capabilities] 2017-08-11T08:11:51Z constructive # prove declarations resolvable diff --git a/packages/function-resolution/revert/schemas/function_resolution/procedures/api_catalog_row.sql b/packages/function-resolution/revert/schemas/function_resolution/procedures/api_catalog_row.sql new file mode 100644 index 000000000..5868d339b --- /dev/null +++ b/packages/function-resolution/revert/schemas/function_resolution/procedures/api_catalog_row.sql @@ -0,0 +1,7 @@ +-- Revert schemas/function_resolution/procedures/api_catalog_row from pg + +BEGIN; + +DROP FUNCTION function_resolution.api_catalog_row(uuid, text, uuid, uuid); + +COMMIT; diff --git a/packages/function-resolution/revert/schemas/function_resolution/procedures/bound_bucket_id.sql b/packages/function-resolution/revert/schemas/function_resolution/procedures/bound_bucket_id.sql new file mode 100644 index 000000000..15d831408 --- /dev/null +++ b/packages/function-resolution/revert/schemas/function_resolution/procedures/bound_bucket_id.sql @@ -0,0 +1,7 @@ +-- Revert schemas/function_resolution/procedures/bound_bucket_id from pg + +BEGIN; + +DROP FUNCTION function_resolution.bound_bucket_id(uuid, text, uuid, uuid, text); + +COMMIT; diff --git a/packages/function-resolution/revert/schemas/function_resolution/procedures/bucket_catalog_row.sql b/packages/function-resolution/revert/schemas/function_resolution/procedures/bucket_catalog_row.sql new file mode 100644 index 000000000..4070b5618 --- /dev/null +++ b/packages/function-resolution/revert/schemas/function_resolution/procedures/bucket_catalog_row.sql @@ -0,0 +1,7 @@ +-- Revert schemas/function_resolution/procedures/bucket_catalog_row from pg + +BEGIN; + +DROP FUNCTION function_resolution.bucket_catalog_row(uuid, text, uuid, uuid); + +COMMIT; diff --git a/packages/function-resolution/revert/schemas/function_resolution/procedures/catalog_location.sql b/packages/function-resolution/revert/schemas/function_resolution/procedures/catalog_location.sql deleted file mode 100644 index c30c102b6..000000000 --- a/packages/function-resolution/revert/schemas/function_resolution/procedures/catalog_location.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Revert schemas/function_resolution/procedures/catalog_location from pg - -BEGIN; - -DROP FUNCTION function_resolution.catalog_location; - -COMMIT; diff --git a/packages/function-resolution/revert/schemas/function_resolution/procedures/definitions_location.sql b/packages/function-resolution/revert/schemas/function_resolution/procedures/definitions_location.sql index 7e0c3377e..672d338a7 100644 --- a/packages/function-resolution/revert/schemas/function_resolution/procedures/definitions_location.sql +++ b/packages/function-resolution/revert/schemas/function_resolution/procedures/definitions_location.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION function_resolution.definitions_location; +DROP FUNCTION function_resolution.definitions_location(uuid, text); COMMIT; diff --git a/packages/function-resolution/revert/schemas/function_resolution/procedures/enqueue.sql b/packages/function-resolution/revert/schemas/function_resolution/procedures/enqueue.sql index 8e457c56c..ee5771693 100644 --- a/packages/function-resolution/revert/schemas/function_resolution/procedures/enqueue.sql +++ b/packages/function-resolution/revert/schemas/function_resolution/procedures/enqueue.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION function_resolution.enqueue; +DROP FUNCTION function_resolution.enqueue(text, json, text, uuid, uuid, text, text, text, timestamptz, int4, int4, uuid, text, bool, text, uuid, uuid); COMMIT; diff --git a/packages/function-resolution/revert/schemas/function_resolution/procedures/frame_candidates.sql b/packages/function-resolution/revert/schemas/function_resolution/procedures/frame_candidates.sql new file mode 100644 index 000000000..4927ba8bc --- /dev/null +++ b/packages/function-resolution/revert/schemas/function_resolution/procedures/frame_candidates.sql @@ -0,0 +1,7 @@ +-- Revert schemas/function_resolution/procedures/frame_candidates from pg + +BEGIN; + +DROP FUNCTION function_resolution.frame_candidates(uuid, text, uuid); + +COMMIT; diff --git a/packages/function-resolution/revert/schemas/function_resolution/procedures/resolve.sql b/packages/function-resolution/revert/schemas/function_resolution/procedures/resolve.sql index 9102e873f..51171171c 100644 --- a/packages/function-resolution/revert/schemas/function_resolution/procedures/resolve.sql +++ b/packages/function-resolution/revert/schemas/function_resolution/procedures/resolve.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION function_resolution.resolve; +DROP FUNCTION function_resolution.resolve(uuid, text, uuid, text, bool); COMMIT; diff --git a/packages/function-resolution/revert/schemas/function_resolution/procedures/resolve_api.sql b/packages/function-resolution/revert/schemas/function_resolution/procedures/resolve_api.sql new file mode 100644 index 000000000..ee3ff0522 --- /dev/null +++ b/packages/function-resolution/revert/schemas/function_resolution/procedures/resolve_api.sql @@ -0,0 +1,7 @@ +-- Revert schemas/function_resolution/procedures/resolve_api from pg + +BEGIN; + +DROP FUNCTION function_resolution.resolve_api(uuid, text, uuid, text); + +COMMIT; diff --git a/packages/function-resolution/revert/schemas/function_resolution/procedures/resolve_bucket.sql b/packages/function-resolution/revert/schemas/function_resolution/procedures/resolve_bucket.sql new file mode 100644 index 000000000..b3239cfae --- /dev/null +++ b/packages/function-resolution/revert/schemas/function_resolution/procedures/resolve_bucket.sql @@ -0,0 +1,7 @@ +-- Revert schemas/function_resolution/procedures/resolve_bucket from pg + +BEGIN; + +DROP FUNCTION function_resolution.resolve_bucket(uuid, text, uuid, text[], text); + +COMMIT; diff --git a/packages/function-resolution/revert/schemas/function_resolution/procedures/resolve_capabilities.sql b/packages/function-resolution/revert/schemas/function_resolution/procedures/resolve_capabilities.sql new file mode 100644 index 000000000..07be079b9 --- /dev/null +++ b/packages/function-resolution/revert/schemas/function_resolution/procedures/resolve_capabilities.sql @@ -0,0 +1,7 @@ +-- Revert schemas/function_resolution/procedures/resolve_capabilities from pg + +BEGIN; + +DROP FUNCTION function_resolution.resolve_capabilities(uuid, text, uuid, uuid, text, uuid, jsonb, text); + +COMMIT; diff --git a/packages/function-resolution/revert/schemas/function_resolution/procedures/resolve_invocation.sql b/packages/function-resolution/revert/schemas/function_resolution/procedures/resolve_invocation.sql index a414eb46d..3678d461f 100644 --- a/packages/function-resolution/revert/schemas/function_resolution/procedures/resolve_invocation.sql +++ b/packages/function-resolution/revert/schemas/function_resolution/procedures/resolve_invocation.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION function_resolution.resolve_invocation; +DROP FUNCTION function_resolution.resolve_invocation(uuid, text, uuid, text, uuid, text, uuid); COMMIT; diff --git a/packages/function-resolution/revert/schemas/function_resolution/procedures/resolve_payload_refs.sql b/packages/function-resolution/revert/schemas/function_resolution/procedures/resolve_payload_refs.sql new file mode 100644 index 000000000..fafbfc40c --- /dev/null +++ b/packages/function-resolution/revert/schemas/function_resolution/procedures/resolve_payload_refs.sql @@ -0,0 +1,7 @@ +-- Revert schemas/function_resolution/procedures/resolve_payload_refs from pg + +BEGIN; + +DROP FUNCTION function_resolution.resolve_payload_refs(uuid, text, uuid, jsonb); + +COMMIT; diff --git a/packages/function-resolution/revert/schemas/function_resolution/procedures/routing.sql b/packages/function-resolution/revert/schemas/function_resolution/procedures/routing.sql index f6886ac44..8239eb87e 100644 --- a/packages/function-resolution/revert/schemas/function_resolution/procedures/routing.sql +++ b/packages/function-resolution/revert/schemas/function_resolution/procedures/routing.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION function_resolution.routing; +DROP FUNCTION function_resolution.routing(uuid, uuid); COMMIT; diff --git a/packages/function-resolution/revert/schemas/function_resolution/procedures/validate_capabilities.sql b/packages/function-resolution/revert/schemas/function_resolution/procedures/validate_capabilities.sql new file mode 100644 index 000000000..524ec8826 --- /dev/null +++ b/packages/function-resolution/revert/schemas/function_resolution/procedures/validate_capabilities.sql @@ -0,0 +1,7 @@ +-- Revert schemas/function_resolution/procedures/validate_capabilities from pg + +BEGIN; + +DROP FUNCTION function_resolution.validate_capabilities(uuid, text, uuid, uuid, text, uuid, jsonb, text); + +COMMIT; diff --git a/packages/function-resolution/sql/pgpm-function-resolution--0.36.0.bundle.tar.gz b/packages/function-resolution/sql/pgpm-function-resolution--0.36.0.bundle.tar.gz deleted file mode 100644 index b7a528121..000000000 Binary files a/packages/function-resolution/sql/pgpm-function-resolution--0.36.0.bundle.tar.gz and /dev/null differ diff --git a/packages/function-resolution/sql/pgpm-function-resolution--0.36.0.sql b/packages/function-resolution/sql/pgpm-function-resolution--0.36.0.sql deleted file mode 100644 index 1b1d9802a..000000000 --- a/packages/function-resolution/sql/pgpm-function-resolution--0.36.0.sql +++ /dev/null @@ -1,448 +0,0 @@ -\echo Use "CREATE EXTENSION pgpm-function-resolution" to load this file. \quit -CREATE SCHEMA IF NOT EXISTS function_resolution; - -GRANT USAGE ON SCHEMA function_resolution TO administrator; - -GRANT USAGE ON SCHEMA function_resolution TO authenticated; - -ALTER DEFAULT PRIVILEGES IN SCHEMA function_resolution - GRANT EXECUTE ON FUNCTIONS TO administrator; - -CREATE FUNCTION function_resolution.definitions_location( - database_id uuid, - scope text -) RETURNS TABLE ( - schema_name text, - table_name text, - entity_field text -) AS $EOFCODE$ -DECLARE - v_defs_table_id uuid; - v_entity_field text; -BEGIN - SELECT fm.definitions_table_id, fm.entity_field - INTO v_defs_table_id, v_entity_field - FROM metaschema_modules_public.function_module fm - WHERE fm.database_id = definitions_location.database_id - AND fm.scope = definitions_location.scope; - - IF NOT FOUND OR v_defs_table_id IS NULL THEN - RETURN; - END IF; - - SELECT s.schema_name, t.name - INTO definitions_location.schema_name, definitions_location.table_name - FROM metaschema_public.schema s - JOIN metaschema_public."table" t ON (t.schema_id = s.id AND t.database_id = s.database_id) - WHERE t.id = v_defs_table_id; - - IF NOT FOUND THEN - RETURN; - END IF; - - definitions_location.entity_field := v_entity_field; - RETURN NEXT; -END; -$EOFCODE$ LANGUAGE plpgsql STABLE SECURITY DEFINER; - -CREATE FUNCTION function_resolution.routing( - database_id uuid, - scope text, - function_definition_id uuid -) RETURNS TABLE ( - queue_name text, - priority int, - max_attempts int -) AS $EOFCODE$ -DECLARE - v_schema text; - v_table text; - v_entity_field text; - v_query text; -BEGIN - SELECT l.schema_name, l.table_name, l.entity_field - INTO v_schema, v_table, v_entity_field - FROM function_resolution.definitions_location(routing.database_id, routing.scope) l; - - IF v_schema IS NULL OR function_definition_id IS NULL THEN - RETURN; - END IF; - - -- SELECT queue_name, priority, max_attempts FROM ""."
" WHERE id = $1 - v_query := format( - 'SELECT queue_name, priority, max_attempts FROM %I.%I WHERE id = $1', - v_schema, v_table - ); - - EXECUTE v_query - INTO routing.queue_name, - routing.priority, - routing.max_attempts - USING function_definition_id; - - -- Only surface a routing row when the definition actually exists (a missing - -- id leaves every column NULL — the caller wants defaults, not a NULL row). - IF routing.queue_name IS NULL - AND routing.priority IS NULL - AND routing.max_attempts IS NULL THEN - RETURN; - END IF; - - RETURN NEXT; -END; -$EOFCODE$ LANGUAGE plpgsql STABLE SECURITY DEFINER; - -CREATE FUNCTION function_resolution.catalog_location( - database_id uuid -) RETURNS TABLE ( - schema_name text, - table_name text -) AS $EOFCODE$ -DECLARE - v_functions_table_id uuid; -BEGIN - BEGIN - SELECT cm.functions_table_id - INTO STRICT v_functions_table_id - FROM metaschema_modules_public.catalog_module cm - WHERE cm.database_id = catalog_location.database_id - AND cm.functions_table_id <> uuid_nil(); - EXCEPTION - WHEN NO_DATA_FOUND THEN - RETURN; - WHEN TOO_MANY_ROWS THEN - RAISE EXCEPTION 'FUNCTION_RESOLUTION_CATALOG_AMBIGUOUS: multiple functions catalogs registered for database %', - catalog_location.database_id; - END; - - SELECT s.schema_name, t.name - INTO catalog_location.schema_name, catalog_location.table_name - FROM metaschema_public.schema s - JOIN metaschema_public."table" t ON (t.schema_id = s.id AND t.database_id = s.database_id) - WHERE t.id = v_functions_table_id; - - IF NOT FOUND THEN - RETURN; - END IF; - - RETURN NEXT; -END; -$EOFCODE$ LANGUAGE plpgsql STABLE SECURITY DEFINER; - -CREATE FUNCTION function_resolution.resolve( - database_id uuid, - scope text, - entity_id uuid, - task_identifier text, - require_definition boolean DEFAULT true -) RETURNS TABLE ( - function_definition_id uuid, - resolved_scope text, - owner_database_id uuid -) AS $EOFCODE$ -DECLARE - v_frame_db record; - v_schema text; - v_table text; - v_query text; - v_hit record; - v_best_ord bigint; -BEGIN - -- Group the ordered frames by lookup database, expanding each frame into - -- its probe candidates (exact key first, scope-default second) with a - -- global ordinality that preserves frame precedence across databases. - FOR v_frame_db IN - SELECT c.lookup_database_id, - array_agg(c.owner_scope ORDER BY c.ord) AS scopes, - array_agg(c.owner_key ORDER BY c.ord) AS keys, - array_agg(c.ord ORDER BY c.ord) AS ords - FROM ( - SELECT f.lookup_database_id, - f.scope AS owner_scope, - cand.owner_key, - (f.ord * 2) + cand.off AS ord - FROM app_scope.frames( - resolve.database_id, - resolve.scope, - resolve.entity_id - ) WITH ORDINALITY AS f(scope, lookup_database_id, key_value, ord) - CROSS JOIN LATERAL ( - VALUES (f.key_value, 0::bigint), (NULL::uuid, 1::bigint) - ) AS cand(owner_key, off) - -- Global frames carry no key: emit the NULL candidate once. - WHERE cand.off = 0 OR f.key_value IS NOT NULL - ) c - GROUP BY c.lookup_database_id - ORDER BY min(c.ord) - LOOP - SELECT l.schema_name, l.table_name - INTO v_schema, v_table - FROM function_resolution.catalog_location(v_frame_db.lookup_database_id) l; - - IF v_schema IS NULL THEN - -- No catalog for this frame database. If it hosts function - -- modules the catalog cannot answer for it — fail loud so a - -- missing catalog never silently mis-resolves as "not found". - IF EXISTS ( - SELECT 1 FROM metaschema_modules_public.function_module fm - WHERE fm.database_id = v_frame_db.lookup_database_id - ) THEN - RAISE EXCEPTION USING - errcode = 'FR001', - message = format( - 'FUNCTION_RESOLUTION_CATALOG_UNAVAILABLE: database %s hosts function modules but has no functions catalog', - v_frame_db.lookup_database_id - ); - END IF; - CONTINUE; - END IF; - - -- One indexed read per catalog: LATERAL over the ordered candidates, - -- each branch an exact probe of one partial unique index. - v_query := format( - 'SELECT hit.id, hit.owner_scope, hit.database_id, cand.ord - FROM unnest($2::text[], $3::uuid[], $4::bigint[]) AS cand(owner_scope, owner_key, ord) - CROSS JOIN LATERAL ( - SELECT c.id, c.owner_scope, c.database_id - FROM %I.%I c - WHERE c.task_identifier = $1 - AND c.owner_scope = cand.owner_scope - AND c.owner_key = cand.owner_key - AND cand.owner_key IS NOT NULL - UNION ALL - SELECT c.id, c.owner_scope, c.database_id - FROM %I.%I c - WHERE c.task_identifier = $1 - AND c.owner_scope = cand.owner_scope - AND c.owner_key IS NULL - AND cand.owner_key IS NULL - ) hit - ORDER BY cand.ord - LIMIT 1', - v_schema, v_table, v_schema, v_table - ); - - EXECUTE v_query - INTO v_hit - USING resolve.task_identifier, - v_frame_db.scopes, v_frame_db.keys, v_frame_db.ords; - - IF v_hit.id IS NOT NULL AND (v_best_ord IS NULL OR v_hit.ord < v_best_ord) THEN - v_best_ord := v_hit.ord; - resolve.function_definition_id := v_hit.id; - resolve.resolved_scope := v_hit.owner_scope; - resolve.owner_database_id := v_hit.database_id; - END IF; - END LOOP; - - IF resolve.function_definition_id IS NOT NULL THEN - RETURN NEXT; - RETURN; - END IF; - - -- Chain exhausted. - IF require_definition THEN - RAISE EXCEPTION 'FUNCTION_DEFINITION_NOT_FOUND: no definition for task_identifier "%" in the scope chain starting at scope "%" (database_id=%)', - task_identifier, scope, database_id; - END IF; - - RETURN; -END; -$EOFCODE$ LANGUAGE plpgsql STABLE SECURITY DEFINER; - -CREATE FUNCTION function_resolution.resolve_invocation( - database_id uuid, - scope text, - entity_id uuid, - task_identifier text, - existing_id uuid DEFAULT NULL, - existing_scope text DEFAULT NULL, - api_binding_id uuid DEFAULT NULL -) RETURNS TABLE ( - function_definition_id uuid, - definition_scope text -) AS $EOFCODE$ -DECLARE - v_scope text; - v_lookup_db uuid; - v_probe_key uuid; - v_found uuid; - v_catalog_schema text; - v_catalog_table text; -BEGIN - -- API-provenance path (api_binding_id present) must declare its definition - -- explicitly — the function_callable_check policy verifies the binding - -- against the supplied function_definition_id. Never auto-resolve one for - -- it: a NULL id stays NULL so the policy can deny. - IF existing_id IS NULL AND api_binding_id IS NOT NULL THEN - resolve_invocation.function_definition_id := NULL; - resolve_invocation.definition_scope := NULL; - RETURN NEXT; - RETURN; - END IF; - - IF existing_id IS NOT NULL THEN - v_scope := COALESCE(resolve_invocation.existing_scope, resolve_invocation.scope); - - -- Key/lookup database for the declared scope come from the same frame - -- definition resolution uses, so an `org` pair keys by the owning org - -- and the platform frame probes the platform database. - SELECT f.lookup_database_id, f.key_value - INTO v_lookup_db, v_probe_key - FROM app_scope.frames(resolve_invocation.database_id, resolve_invocation.scope, resolve_invocation.entity_id) f - WHERE f.scope = v_scope; - - IF v_lookup_db IS NULL THEN - v_lookup_db := database_id; - END IF; - - SELECT l.schema_name, l.table_name - INTO v_catalog_schema, v_catalog_table - FROM function_resolution.catalog_location(v_lookup_db) l; - - IF v_catalog_schema IS NULL THEN - RAISE EXCEPTION 'FUNCTION_RESOLUTION_CATALOG_UNAVAILABLE: database % has no functions catalog to validate pair against (task_identifier "%")', - v_lookup_db, task_identifier - USING ERRCODE = 'FR001'; - END IF; - - -- Same two-pass keying as resolution: the exact scope-key row wins, - -- the scope-default (owner_key IS NULL) row only as fallback. - EXECUTE format( - 'SELECT id FROM %I.%I WHERE owner_scope = $1 AND task_identifier = $2 AND owner_key = $3', - v_catalog_schema, v_catalog_table - ) INTO v_found USING v_scope, task_identifier, v_probe_key; - IF v_found IS NULL THEN - EXECUTE format( - 'SELECT id FROM %I.%I WHERE owner_scope = $1 AND task_identifier = $2 AND owner_key IS NULL', - v_catalog_schema, v_catalog_table - ) INTO v_found USING v_scope, task_identifier; - END IF; - - IF v_found IS DISTINCT FROM existing_id THEN - RAISE EXCEPTION 'FUNCTION_DEFINITION_INVALID_PAIR: function_definition_id % / definition_scope "%" does not resolve to task_identifier "%" (database_id=%)', - existing_id, v_scope, task_identifier, database_id; - END IF; - - resolve_invocation.function_definition_id := existing_id; - resolve_invocation.definition_scope := v_scope; - RETURN NEXT; - RETURN; - END IF; - - -- No pair supplied: resolve across the scope chain. Definition-less stays - -- allowed, so a miss leaves both columns NULL rather than raising. - SELECT r.function_definition_id, r.resolved_scope - INTO resolve_invocation.function_definition_id, resolve_invocation.definition_scope - FROM function_resolution.resolve( - resolve_invocation.database_id, resolve_invocation.scope, resolve_invocation.entity_id, resolve_invocation.task_identifier, false - ) r; - - RETURN NEXT; -END; -$EOFCODE$ LANGUAGE plpgsql VOLATILE SECURITY DEFINER; - -CREATE FUNCTION function_resolution.enqueue( - task_identifier text, - payload pg_catalog.json DEFAULT '{}'::json, - scope text DEFAULT NULL, - entity_id uuid DEFAULT NULL, - function_definition_id uuid DEFAULT NULL, - definition_scope text DEFAULT NULL, - job_key text DEFAULT NULL, - queue_name text DEFAULT NULL, - run_at timestamptz DEFAULT now(), - max_attempts int DEFAULT NULL, - priority int DEFAULT NULL, - organization_id uuid DEFAULT NULL, - entity_type text DEFAULT NULL, - should_resolve boolean DEFAULT true, - resolution_scope text DEFAULT NULL, - resolution_key uuid DEFAULT NULL, - db_id uuid DEFAULT jwt_private.current_database_id() -) RETURNS app_jobs.jobs AS $EOFCODE$ -DECLARE - v_database_id uuid; - v_exec_scope text; - v_resolution_scope text; - v_resolution_key uuid; - v_fn_id uuid; - v_def_scope text; - v_defs_db uuid; - v_queue_name text; - v_priority integer; - v_max_attempts integer; -BEGIN - v_database_id := db_id; - - -- Hard contract: the execution scope is a provisioning-time constant at the - -- call site, never silently defaulted (constructive-planning #1183). - IF scope IS NULL THEN - RAISE EXCEPTION 'ENQUEUE_SCOPE_REQUIRED: scope is required (no default scope)'; - END IF; - v_exec_scope := scope; - - -- The resolution walk starts from a scope-key SEPARATE from the billing - -- entity_id. It defaults to the execution scope/entity so the invocation lane - -- (which supplies a stamped pair and never resolves here) is unchanged. - v_resolution_scope := COALESCE(resolution_scope, scope); - v_resolution_key := COALESCE(resolution_key, entity_id); - - v_fn_id := function_definition_id; - v_def_scope := definition_scope; - - -- Resolve the winning definition when the caller did not already stamp one. - -- resolve() also returns the definition's home database, so the resolving - -- lane needs no second frame walk below. - IF v_fn_id IS NULL AND should_resolve THEN - SELECT r.function_definition_id, r.resolved_scope, r.owner_database_id - INTO v_fn_id, v_def_scope, v_defs_db - FROM function_resolution.resolve( - v_database_id, v_resolution_scope, v_resolution_key, task_identifier, false - ) r; - END IF; - - -- Definition routing: read queue_name/priority/max_attempts from the exact - -- winning definition (no scope-chain re-walk). The definition's home database - -- is the frame's lookup_database_id for its scope (the platform database for a - -- platform-scope definition, the execution database otherwise); only the - -- caller-supplied-pair lane still derives it from the declared scope's frame. - IF v_fn_id IS NOT NULL AND v_def_scope IS NOT NULL THEN - IF v_defs_db IS NULL THEN - SELECT f.lookup_database_id - INTO v_defs_db - FROM app_scope.frames(v_database_id, v_resolution_scope, v_resolution_key) f - WHERE f.scope = v_def_scope - LIMIT 1; - END IF; - - IF v_defs_db IS NULL THEN - v_defs_db := v_database_id; - END IF; - - SELECT rt.queue_name, rt.priority, rt.max_attempts - INTO v_queue_name, v_priority, v_max_attempts - FROM function_resolution.routing(v_defs_db, v_def_scope, v_fn_id) rt; - END IF; - - -- Caller-supplied routing always wins over the definition's; the definition's - -- wins over the add_job hard defaults. - RETURN app_jobs.add_job( - identifier := task_identifier, - payload := COALESCE(payload, '{}'::json), - job_key := job_key, - queue_name := COALESCE(queue_name, v_queue_name), - run_at := COALESCE(run_at, now()), - max_attempts := COALESCE(max_attempts, v_max_attempts, 25), - priority := COALESCE(priority, v_priority, 0), - entity_id := entity_id, - organization_id := organization_id, - entity_type := entity_type, - function_definition_id := v_fn_id, - definition_scope := v_def_scope, - db_id := v_database_id - ); -END; -$EOFCODE$ LANGUAGE plpgsql VOLATILE SECURITY DEFINER; - -COMMENT ON FUNCTION function_resolution.enqueue(text, pg_catalog.json, text, uuid, uuid, text, text, text, timestamptz, int, int, uuid, text, boolean, text, uuid, uuid) IS 'Resolver-aware job enqueue: resolves (or trusts a supplied) function definition for the execution (database, scope, entity, task_identifier), stamps the (function_definition_id, definition_scope) pair and the definition''s queue routing, then delegates the insert to app_jobs.add_job. The single enqueue path for function jobs; definition-less tasks enqueue with a NULL pair. Portable: built only on app_scope + the metaschema catalog + app_jobs, no AST/deparser runtime.'; \ No newline at end of file diff --git a/packages/function-resolution/sql/pgpm-function-resolution--0.39.0.bundle.tar.gz b/packages/function-resolution/sql/pgpm-function-resolution--0.39.0.bundle.tar.gz new file mode 100644 index 000000000..150d8b98e Binary files /dev/null and b/packages/function-resolution/sql/pgpm-function-resolution--0.39.0.bundle.tar.gz differ diff --git a/packages/function-resolution/sql/pgpm-function-resolution--0.39.0.sql b/packages/function-resolution/sql/pgpm-function-resolution--0.39.0.sql new file mode 100644 index 000000000..b11f8e161 --- /dev/null +++ b/packages/function-resolution/sql/pgpm-function-resolution--0.39.0.sql @@ -0,0 +1,1379 @@ +\echo Use "CREATE EXTENSION pgpm-function-resolution" to load this file. \quit +CREATE SCHEMA IF NOT EXISTS function_resolution; + +GRANT USAGE ON SCHEMA function_resolution TO administrator; + +GRANT USAGE ON SCHEMA function_resolution TO authenticated; + +ALTER DEFAULT PRIVILEGES IN SCHEMA function_resolution + GRANT EXECUTE ON FUNCTIONS TO administrator; + +CREATE FUNCTION function_resolution.definitions_location( + database_id uuid, + scope text +) RETURNS TABLE ( + schema_name text, + table_name text, + entity_field text +) AS $EOFCODE$ +DECLARE + v_defs_table_id uuid; + v_entity_field text; +BEGIN + SELECT fm.definitions_table_id, fm.entity_field + INTO v_defs_table_id, v_entity_field + FROM metaschema_modules_public.function_module fm + WHERE fm.database_id = definitions_location.database_id + AND fm.scope = definitions_location.scope; + + IF NOT FOUND OR v_defs_table_id IS NULL THEN + RETURN; + END IF; + + SELECT s.schema_name, t.name + INTO definitions_location.schema_name, definitions_location.table_name + FROM metaschema_public.schema s + JOIN metaschema_public."table" t ON (t.schema_id = s.id AND t.database_id = s.database_id) + WHERE t.id = v_defs_table_id; + + IF NOT FOUND THEN + RETURN; + END IF; + + definitions_location.entity_field := v_entity_field; + RETURN NEXT; +END; +$EOFCODE$ LANGUAGE plpgsql STABLE SECURITY DEFINER; + +CREATE FUNCTION function_resolution.routing( + database_id uuid, + function_definition_id uuid +) RETURNS TABLE ( + queue_name text, + priority int, + max_attempts int +) AS $EOFCODE$ +BEGIN + IF function_definition_id IS NULL THEN + RETURN; + END IF; + + SELECT c.queue_name, c.priority, c.max_attempts + INTO routing.queue_name, + routing.priority, + routing.max_attempts + FROM catalog_private.functions c + WHERE c.id = routing.function_definition_id + AND c.database_id = routing.database_id; + + -- Only surface a routing row when the definition actually exists (a missing + -- id leaves every column NULL — the caller wants defaults, not a NULL row). + IF routing.queue_name IS NULL + AND routing.priority IS NULL + AND routing.max_attempts IS NULL THEN + RETURN; + END IF; + + RETURN NEXT; +END; +$EOFCODE$ LANGUAGE plpgsql STABLE SECURITY DEFINER; + +CREATE FUNCTION function_resolution.resolve( + database_id uuid, + scope text, + entity_id uuid, + task_identifier text, + require_definition boolean DEFAULT true +) RETURNS TABLE ( + function_definition_id uuid, + resolved_scope text, + owner_database_id uuid +) AS $EOFCODE$ +DECLARE + v_unanswerable uuid; + v_hit record; +BEGIN + -- A frame database that hosts function modules but never deployed a catalog + -- module has definitions the catalog cannot see. Answering "not found" there + -- would be a wrong answer, so it is raised before any probe. + SELECT f.lookup_database_id + INTO v_unanswerable + FROM app_scope.frames( + resolve.database_id, + resolve.scope, + resolve.entity_id + ) AS f(scope, lookup_database_id, key_value) + WHERE EXISTS ( + SELECT 1 FROM metaschema_modules_public.function_module fm + WHERE fm.database_id = f.lookup_database_id + ) + AND NOT EXISTS ( + SELECT 1 + FROM metaschema_modules_public.catalog_module cm + WHERE cm.database_id = f.lookup_database_id + AND cm.functions_table_id IS NOT NULL + AND cm.functions_table_id <> uuid_nil() + ) + LIMIT 1; + + IF v_unanswerable IS NOT NULL THEN + RAISE EXCEPTION USING + errcode = 'FR001', + message = format( + 'FUNCTION_RESOLUTION_CATALOG_UNAVAILABLE: database %s hosts function modules but has no functions catalog', + v_unanswerable + ); + END IF; + + -- One indexed read: LATERAL over the ordered candidates, each branch an + -- exact probe of one partial unique index. + SELECT hit.id, hit.owner_scope, hit.database_id + INTO v_hit + FROM ( + SELECT f.lookup_database_id, + f.scope AS owner_scope, + cand.owner_key, + (f.ord * 2) + cand.off AS ord + FROM app_scope.frames( + resolve.database_id, + resolve.scope, + resolve.entity_id + ) WITH ORDINALITY AS f(scope, lookup_database_id, key_value, ord) + CROSS JOIN LATERAL ( + VALUES (f.key_value, 0::bigint), (NULL::uuid, 1::bigint) + ) AS cand(owner_key, off) + -- Global frames carry no key: emit the NULL candidate once. + WHERE cand.off = 0 OR f.key_value IS NOT NULL + ) cand + CROSS JOIN LATERAL ( + SELECT c.id, c.owner_scope, c.database_id + FROM catalog_private.functions c + WHERE c.task_identifier = resolve.task_identifier + AND c.owner_scope = cand.owner_scope + AND c.owner_key = cand.owner_key + AND cand.owner_key IS NOT NULL + AND c.database_id = CASE + WHEN cand.owner_scope = 'database' THEN cand.owner_key + ELSE cand.lookup_database_id + END + UNION ALL + SELECT c.id, c.owner_scope, c.database_id + FROM catalog_private.functions c + WHERE c.task_identifier = resolve.task_identifier + AND c.owner_scope = cand.owner_scope + AND c.owner_key IS NULL + AND cand.owner_key IS NULL + AND c.database_id = cand.lookup_database_id + ) hit + ORDER BY cand.ord + LIMIT 1; + + IF v_hit.id IS NOT NULL THEN + resolve.function_definition_id := v_hit.id; + resolve.resolved_scope := v_hit.owner_scope; + resolve.owner_database_id := v_hit.database_id; + RETURN NEXT; + RETURN; + END IF; + + -- Chain exhausted. + IF require_definition THEN + RAISE EXCEPTION 'FUNCTION_DEFINITION_NOT_FOUND: no definition for task_identifier "%" in the scope chain starting at scope "%" (database_id=%)', + task_identifier, scope, database_id; + END IF; + + RETURN; +END; +$EOFCODE$ LANGUAGE plpgsql STABLE SECURITY DEFINER; + +CREATE FUNCTION function_resolution.resolve_invocation( + database_id uuid, + scope text, + entity_id uuid, + task_identifier text, + existing_id uuid DEFAULT NULL, + existing_scope text DEFAULT NULL, + api_binding_id uuid DEFAULT NULL +) RETURNS TABLE ( + function_definition_id uuid, + definition_scope text +) AS $EOFCODE$ +DECLARE + v_scope text; + v_lookup_db uuid; + v_probe_key uuid; + v_found uuid; + v_expected_db uuid; +BEGIN + -- API-provenance path (api_binding_id present) must declare its definition + -- explicitly — the function_callable_check policy verifies the binding + -- against the supplied function_definition_id. Never auto-resolve one for + -- it: a NULL id stays NULL so the policy can deny. + IF existing_id IS NULL AND api_binding_id IS NOT NULL THEN + resolve_invocation.function_definition_id := NULL; + resolve_invocation.definition_scope := NULL; + RETURN NEXT; + RETURN; + END IF; + + IF existing_id IS NOT NULL THEN + v_scope := COALESCE(resolve_invocation.existing_scope, resolve_invocation.scope); + + -- Key/lookup database for the declared scope come from the same frame + -- definition resolution uses, so an `org` pair keys by the owning org + -- and the platform frame probes the platform database. + SELECT f.lookup_database_id, f.key_value + INTO v_lookup_db, v_probe_key + FROM app_scope.frames(resolve_invocation.database_id, resolve_invocation.scope, resolve_invocation.entity_id) f + WHERE f.scope = v_scope; + + IF v_lookup_db IS NULL THEN + v_lookup_db := database_id; + END IF; + + IF NOT EXISTS ( + SELECT 1 + FROM metaschema_modules_public.catalog_module cm + WHERE cm.database_id = v_lookup_db + AND cm.functions_table_id IS NOT NULL + AND cm.functions_table_id <> uuid_nil() + ) THEN + RAISE EXCEPTION 'FUNCTION_RESOLUTION_CATALOG_UNAVAILABLE: database % has no functions catalog to validate pair against (task_identifier "%")', + v_lookup_db, task_identifier + USING ERRCODE = 'FR001'; + END IF; + + -- Same two-pass keying as resolution: the exact scope-key row wins, the + -- scope-default (owner_key IS NULL) row only as fallback. Both carry the + -- shared plane's row-identity predicate. + v_expected_db := CASE + WHEN v_scope = 'database' AND v_probe_key IS NOT NULL THEN v_probe_key + ELSE v_lookup_db + END; + + SELECT c.id + INTO v_found + FROM catalog_private.functions c + WHERE c.owner_scope = v_scope + AND c.task_identifier = resolve_invocation.task_identifier + AND c.owner_key = v_probe_key + AND c.database_id = v_expected_db; + + IF v_found IS NULL THEN + SELECT c.id + INTO v_found + FROM catalog_private.functions c + WHERE c.owner_scope = v_scope + AND c.task_identifier = resolve_invocation.task_identifier + AND c.owner_key IS NULL + AND c.database_id = v_lookup_db; + END IF; + + IF v_found IS DISTINCT FROM existing_id THEN + RAISE EXCEPTION 'FUNCTION_DEFINITION_INVALID_PAIR: function_definition_id % / definition_scope "%" does not resolve to task_identifier "%" (database_id=%)', + existing_id, v_scope, task_identifier, database_id; + END IF; + + resolve_invocation.function_definition_id := existing_id; + resolve_invocation.definition_scope := v_scope; + RETURN NEXT; + RETURN; + END IF; + + -- No pair supplied: resolve across the scope chain. Definition-less stays + -- allowed, so a miss leaves both columns NULL rather than raising. + SELECT r.function_definition_id, r.resolved_scope + INTO resolve_invocation.function_definition_id, resolve_invocation.definition_scope + FROM function_resolution.resolve( + resolve_invocation.database_id, resolve_invocation.scope, resolve_invocation.entity_id, resolve_invocation.task_identifier, false + ) r; + + RETURN NEXT; +END; +$EOFCODE$ LANGUAGE plpgsql VOLATILE SECURITY DEFINER; + +CREATE FUNCTION function_resolution.enqueue( + task_identifier text, + payload pg_catalog.json DEFAULT '{}'::json, + scope text DEFAULT NULL, + entity_id uuid DEFAULT NULL, + function_definition_id uuid DEFAULT NULL, + definition_scope text DEFAULT NULL, + job_key text DEFAULT NULL, + queue_name text DEFAULT NULL, + run_at timestamptz DEFAULT now(), + max_attempts int DEFAULT NULL, + priority int DEFAULT NULL, + organization_id uuid DEFAULT NULL, + entity_type text DEFAULT NULL, + should_resolve boolean DEFAULT true, + resolution_scope text DEFAULT NULL, + resolution_key uuid DEFAULT NULL, + db_id uuid DEFAULT jwt_private.current_database_id() +) RETURNS app_jobs.jobs AS $EOFCODE$ +DECLARE + v_database_id uuid; + v_exec_scope text; + v_resolution_scope text; + v_resolution_key uuid; + v_fn_id uuid; + v_def_scope text; + v_defs_db uuid; + v_queue_name text; + v_priority integer; + v_max_attempts integer; +BEGIN + v_database_id := db_id; + + -- Hard contract: the execution scope is a provisioning-time constant at the + -- call site, never silently defaulted (constructive-planning #1183). + IF scope IS NULL THEN + RAISE EXCEPTION 'ENQUEUE_SCOPE_REQUIRED: scope is required (no default scope)'; + END IF; + v_exec_scope := scope; + + -- The resolution walk starts from a scope-key SEPARATE from the billing + -- entity_id. It defaults to the execution scope/entity so the invocation lane + -- (which supplies a stamped pair and never resolves here) is unchanged. + v_resolution_scope := COALESCE(resolution_scope, scope); + v_resolution_key := COALESCE(resolution_key, entity_id); + + v_fn_id := function_definition_id; + v_def_scope := definition_scope; + + -- Resolve the winning definition when the caller did not already stamp one. + -- resolve() also returns the definition's home database, so the resolving + -- lane needs no second frame walk below. + IF v_fn_id IS NULL AND should_resolve THEN + SELECT r.function_definition_id, r.resolved_scope, r.owner_database_id + INTO v_fn_id, v_def_scope, v_defs_db + FROM function_resolution.resolve( + v_database_id, v_resolution_scope, v_resolution_key, task_identifier, false + ) r; + END IF; + + -- Definition routing: read queue_name/priority/max_attempts from the exact + -- winning definition (no scope-chain re-walk). The definition's home database + -- is the frame's lookup_database_id for its scope (the platform database for a + -- platform-scope definition, the execution database otherwise); only the + -- caller-supplied-pair lane still derives it from the declared scope's frame. + IF v_fn_id IS NOT NULL AND v_def_scope IS NOT NULL THEN + IF v_defs_db IS NULL THEN + SELECT f.lookup_database_id + INTO v_defs_db + FROM app_scope.frames(v_database_id, v_resolution_scope, v_resolution_key) f + WHERE f.scope = v_def_scope + LIMIT 1; + END IF; + + IF v_defs_db IS NULL THEN + v_defs_db := v_database_id; + END IF; + + SELECT rt.queue_name, rt.priority, rt.max_attempts + INTO v_queue_name, v_priority, v_max_attempts + FROM function_resolution.routing(v_defs_db, v_fn_id) rt; + END IF; + + -- Caller-supplied routing always wins over the definition's; the definition's + -- wins over the add_job hard defaults. + RETURN app_jobs.add_job( + identifier := task_identifier, + payload := COALESCE(payload, '{}'::json), + job_key := job_key, + queue_name := COALESCE(queue_name, v_queue_name), + run_at := COALESCE(run_at, now()), + max_attempts := COALESCE(max_attempts, v_max_attempts, 25), + priority := COALESCE(priority, v_priority, 0), + entity_id := entity_id, + organization_id := organization_id, + entity_type := entity_type, + function_definition_id := v_fn_id, + definition_scope := v_def_scope, + db_id := v_database_id + ); +END; +$EOFCODE$ LANGUAGE plpgsql VOLATILE SECURITY DEFINER; + +COMMENT ON FUNCTION function_resolution.enqueue(text, pg_catalog.json, text, uuid, uuid, text, text, text, timestamptz, int, int, uuid, text, boolean, text, uuid, uuid) IS 'Resolver-aware job enqueue: resolves (or trusts a supplied) function definition for the execution (database, scope, entity, task_identifier), stamps the (function_definition_id, definition_scope) pair and the definition''s queue routing, then delegates the insert to app_jobs.add_job. The single enqueue path for function jobs; definition-less tasks enqueue with a NULL pair. Portable: built only on app_scope + the metaschema catalog + app_jobs, no AST/deparser runtime.'; + +CREATE FUNCTION function_resolution.frame_candidates( + database_id uuid, + scope text, + entity_id uuid DEFAULT NULL +) RETURNS TABLE ( + lookup_database_id uuid, + owner_scope text, + owner_key uuid, + ord bigint +) AS $EOFCODE$ +BEGIN + RETURN QUERY + SELECT f.lookup_database_id, + f.scope, + cand.owner_key, + (f.ord * 2) + cand.off + FROM app_scope.frames( + frame_candidates.database_id, + frame_candidates.scope, + frame_candidates.entity_id + ) WITH ORDINALITY AS f(scope, lookup_database_id, key_value, ord) + CROSS JOIN LATERAL ( + VALUES (f.key_value, 0::bigint), (NULL::uuid, 1::bigint) + ) AS cand(owner_key, off) + -- Global frames carry no key: emit the NULL candidate once. + WHERE cand.off = 0 OR f.key_value IS NOT NULL + ORDER BY (f.ord * 2) + cand.off; +END; +$EOFCODE$ LANGUAGE plpgsql STABLE SECURITY DEFINER; + +CREATE FUNCTION function_resolution.resolve_bucket( + database_id uuid, + scope text, + entity_id uuid, + tags text[], + type_filter text DEFAULT NULL +) RETURNS TABLE ( + bucket_id uuid, + bucket_key text, + bucket_type text, + physical_name text, + owner_database_id uuid, + owner_scope text, + owner_key uuid +) AS $EOFCODE$ +DECLARE + v_matches jsonb; + v_match jsonb; +BEGIN + IF resolve_bucket.tags IS NULL OR cardinality(resolve_bucket.tags) = 0 THEN + RAISE EXCEPTION 'CAPABILITY_BUCKET_SELECTOR_EMPTY: a bucket selector needs at least one tag (database_id=%, scope="%")', + resolve_bucket.database_id, resolve_bucket.scope + USING ERRCODE = 'FR010'; + END IF; + + -- Every frame in one indexed read, keeping only the matches of the most + -- specific frame that answered: a nearer frame outranks an outer one, and + -- ties within that frame are the ambiguity raised below. + WITH hits AS ( + SELECT b.id, + b.key, + b.type, + b.physical_name, + b.database_id, + b.owner_scope, + b.owner_key, + cand.ord + FROM function_resolution.frame_candidates( + resolve_bucket.database_id, + resolve_bucket.scope, + resolve_bucket.entity_id + ) cand + JOIN catalog_private.buckets b + ON b.owner_scope = cand.owner_scope + AND b.owner_key IS NOT DISTINCT FROM cand.owner_key + AND b.database_id = CASE + WHEN cand.owner_scope = 'database' THEN cand.owner_key + ELSE cand.lookup_database_id + END + WHERE b.tags @> resolve_bucket.tags + AND (resolve_bucket.type_filter IS NULL OR b.type = resolve_bucket.type_filter) + AND (b.database_id = resolve_bucket.database_id OR b.is_visible) + ), + nearest AS ( + SELECT h.* + FROM hits h + WHERE h.ord = (SELECT min(hh.ord) FROM hits hh) + ) + SELECT COALESCE(jsonb_agg(to_jsonb(n) ORDER BY n.id), '[]'::jsonb) + INTO v_matches + FROM nearest n; + + IF jsonb_array_length(v_matches) = 0 THEN + RAISE EXCEPTION 'CAPABILITY_BUCKET_NOT_FOUND: no bucket tagged % % resolves in the scope chain starting at scope "%" (database_id=%)', + resolve_bucket.tags, + COALESCE('of type ' || resolve_bucket.type_filter, '(any type)'), + resolve_bucket.scope, + resolve_bucket.database_id + USING ERRCODE = 'FR011'; + END IF; + + IF jsonb_array_length(v_matches) > 1 THEN + RAISE EXCEPTION 'CAPABILITY_BUCKET_AMBIGUOUS: % buckets tagged % % resolve equally (candidates: %); retag, narrow by type, or bind the capability explicitly', + jsonb_array_length(v_matches), + resolve_bucket.tags, + COALESCE('of type ' || resolve_bucket.type_filter, '(any type)'), + (SELECT string_agg(format('%s (%s)', m->>'key', m->>'id'), ', ' ORDER BY m->>'key') + FROM jsonb_array_elements(v_matches) m) + USING ERRCODE = 'FR012'; + END IF; + + v_match := v_matches->0; + + resolve_bucket.bucket_id := (v_match->>'id')::uuid; + resolve_bucket.bucket_key := v_match->>'key'; + resolve_bucket.bucket_type := v_match->>'type'; + resolve_bucket.physical_name := v_match->>'physical_name'; + resolve_bucket.owner_database_id := (v_match->>'database_id')::uuid; + resolve_bucket.owner_scope := v_match->>'owner_scope'; + resolve_bucket.owner_key := (v_match->>'owner_key')::uuid; + + RETURN NEXT; +END; +$EOFCODE$ LANGUAGE plpgsql STABLE SECURITY DEFINER; + +CREATE FUNCTION function_resolution.bucket_catalog_row( + database_id uuid, + scope text, + entity_id uuid, + bucket_id uuid +) RETURNS TABLE ( + bucket_key text, + bucket_type text, + physical_name text, + owner_database_id uuid, + owner_scope text, + owner_key uuid +) AS $EOFCODE$ +BEGIN + RETURN QUERY + SELECT b.key, + b.type, + b.physical_name, + b.database_id, + b.owner_scope, + b.owner_key + FROM function_resolution.frame_candidates( + bucket_catalog_row.database_id, + bucket_catalog_row.scope, + bucket_catalog_row.entity_id + ) cand + JOIN catalog_private.buckets b + ON b.owner_scope = cand.owner_scope + AND b.owner_key IS NOT DISTINCT FROM cand.owner_key + AND b.database_id = CASE + WHEN cand.owner_scope = 'database' THEN cand.owner_key + ELSE cand.lookup_database_id + END + WHERE b.id = bucket_catalog_row.bucket_id + AND (b.database_id = bucket_catalog_row.database_id OR b.is_visible) + ORDER BY cand.ord + LIMIT 1; +END; +$EOFCODE$ LANGUAGE plpgsql STABLE SECURITY DEFINER; + +CREATE FUNCTION function_resolution.api_catalog_row( + database_id uuid, + scope text, + entity_id uuid, + api_id uuid +) RETURNS TABLE ( + api_name text, + owner_database_id uuid, + owner_scope text, + owner_key uuid +) AS $EOFCODE$ +BEGIN + RETURN QUERY + SELECT a.name, + a.database_id, + a.owner_scope, + a.owner_key + FROM function_resolution.frame_candidates( + api_catalog_row.database_id, + api_catalog_row.scope, + api_catalog_row.entity_id + ) cand + JOIN catalog_private.apis a + ON a.owner_scope = cand.owner_scope + AND a.owner_key IS NOT DISTINCT FROM cand.owner_key + AND a.database_id = CASE + WHEN cand.owner_scope = 'database' THEN cand.owner_key + ELSE cand.lookup_database_id + END + WHERE a.id = api_catalog_row.api_id + AND (a.database_id = api_catalog_row.database_id OR a.is_visible) + ORDER BY cand.ord + LIMIT 1; +END; +$EOFCODE$ LANGUAGE plpgsql STABLE SECURITY DEFINER; + +CREATE FUNCTION function_resolution.resolve_api( + database_id uuid, + scope text, + entity_id uuid, + selector text +) RETURNS TABLE ( + api_id uuid, + api_name text, + owner_database_id uuid, + owner_scope text, + owner_key uuid +) AS $EOFCODE$ +DECLARE + v_kind text; + v_ref text; + v_api_filter text; + v_scope_filter text; + v_module_table text; + v_module_relid oid; + v_frame record; + v_query text; + v_row record; + v_matches jsonb := '[]'::jsonb; + v_match jsonb; + v_module_schema_id uuid; + v_module_api_name text; + v_module_has_scope boolean; + v_apis_schema text; + v_apis_table text; + v_api_schemas_schema text; + v_api_schemas_table text; + v_resolved record; +BEGIN + IF resolve_api.selector IS NULL OR btrim(resolve_api.selector) = '' THEN + RAISE EXCEPTION 'CAPABILITY_API_SELECTOR_EMPTY: an api selector is required (database_id=%, scope="%")', + resolve_api.database_id, resolve_api.scope + USING ERRCODE = 'FR020'; + END IF; + + -- [.][@] | + v_ref := btrim(resolve_api.selector); + + v_scope_filter := nullif(split_part(v_ref, '@', 2), ''); + v_ref := split_part(v_ref, '@', 1); + + IF split_part(v_ref, '.', 1) LIKE '%\_module' THEN + v_kind := 'module'; + v_api_filter := nullif(substr(v_ref, length(split_part(v_ref, '.', 1)) + 2), ''); + v_ref := split_part(v_ref, '.', 1); + ELSE + v_kind := 'name'; + + IF v_ref LIKE '%.%' OR v_scope_filter IS NOT NULL THEN + RAISE EXCEPTION 'CAPABILITY_API_SELECTOR_INVALID: selector "%" qualifies an api name; only a module selector (_module) takes .api or @scope suffixes', + resolve_api.selector + USING ERRCODE = 'FR020'; + END IF; + END IF; + + IF btrim(coalesce(v_ref, '')) = '' THEN + RAISE EXCEPTION 'CAPABILITY_API_SELECTOR_INVALID: selector "%" names nothing', + resolve_api.selector + USING ERRCODE = 'FR020'; + END IF; + + -- ========================================================================= + -- — one indexed read of the apis catalog for every frame + -- ========================================================================= + IF v_kind = 'name' THEN + -- Keeps only the matches of the most specific frame that answered: a + -- nearer frame outranks an outer one, and ties within that frame are the + -- ambiguity raised below. + WITH hits AS ( + SELECT a.id, + a.name, + a.database_id, + a.owner_scope, + a.owner_key, + cand.ord + FROM function_resolution.frame_candidates( + resolve_api.database_id, + resolve_api.scope, + resolve_api.entity_id + ) cand + JOIN catalog_private.apis a + ON a.owner_scope = cand.owner_scope + AND a.owner_key IS NOT DISTINCT FROM cand.owner_key + AND a.database_id = CASE + WHEN cand.owner_scope = 'database' THEN cand.owner_key + ELSE cand.lookup_database_id + END + WHERE a.name = v_ref + AND (a.database_id = resolve_api.database_id OR a.is_visible) + ), + nearest AS ( + SELECT h.* + FROM hits h + WHERE h.ord = (SELECT min(hh.ord) FROM hits hh) + ) + SELECT COALESCE(jsonb_agg(to_jsonb(n) ORDER BY n.id), '[]'::jsonb) + INTO v_matches + FROM nearest n; + + IF jsonb_array_length(v_matches) = 0 THEN + RAISE EXCEPTION 'CAPABILITY_API_NOT_FOUND: no api named "%" resolves in the scope chain starting at scope "%" (database_id=%)', + v_ref, resolve_api.scope, resolve_api.database_id + USING ERRCODE = 'FR021'; + END IF; + + IF jsonb_array_length(v_matches) > 1 THEN + RAISE EXCEPTION 'CAPABILITY_API_AMBIGUOUS: % apis named "%" resolve equally (candidates: %)', + jsonb_array_length(v_matches), + v_ref, + (SELECT string_agg(format('%s (%s)', m->>'name', m->>'id'), ', ' ORDER BY m->>'id') + FROM jsonb_array_elements(v_matches) m) + USING ERRCODE = 'FR022'; + END IF; + + v_match := v_matches->0; + + resolve_api.api_id := (v_match->>'id')::uuid; + resolve_api.api_name := v_match->>'name'; + resolve_api.owner_database_id := (v_match->>'database_id')::uuid; + resolve_api.owner_scope := v_match->>'owner_scope'; + resolve_api.owner_key := (v_match->>'owner_key')::uuid; + + RETURN NEXT; + RETURN; + END IF; + + -- ========================================================================= + -- [.][@] — resolve through the module's api attachment + -- ========================================================================= + v_module_table := v_ref; + + -- The selector names a module registration table, and a name that is not one + -- must fail as a typo rather than as "not provisioned". Looked up in the + -- catalog by name as an ordinary join: the relation is identified by a bind + -- parameter, so no identifier is interpolated into SQL, and the oid is then + -- reused for the column checks below instead of being probed again. + SELECT c.oid + INTO v_module_relid + FROM pg_class c + JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE n.nspname = 'metaschema_modules_public' + AND c.relname = v_module_table + AND c.relkind IN ('r', 'p', 'v', 'm', 'f'); + + IF v_module_relid IS NULL THEN + RAISE EXCEPTION 'CAPABILITY_API_MODULE_UNKNOWN: selector "%" names module "%", which has no registration table in metaschema_modules_public', + resolve_api.selector, v_ref + USING ERRCODE = 'FR023'; + END IF; + + -- Not every module registration is scoped: a module whose schemas can only + -- be attached at one scope has no scope column, and asking for one turns a + -- valid selector into an error. + SELECT EXISTS ( + SELECT 1 + FROM pg_attribute a + WHERE a.attrelid = v_module_relid + AND a.attname = 'scope' + AND a.attnum > 0 + AND NOT a.attisdropped + ) + INTO v_module_has_scope; + + IF v_scope_filter IS NOT NULL AND NOT v_module_has_scope THEN + RAISE EXCEPTION 'CAPABILITY_API_SELECTOR_INVALID: selector "%" asks for scope "%", but module "%" registers at a single scope', + resolve_api.selector, v_scope_filter, v_ref + USING ERRCODE = 'FR020'; + END IF; + + IF NOT EXISTS ( + SELECT 1 + FROM pg_attribute a + WHERE a.attrelid = v_module_relid + AND a.attname = 'api_name' + AND a.attnum > 0 + AND NOT a.attisdropped + ) THEN + RAISE EXCEPTION 'CAPABILITY_API_MODULE_UNSUPPORTED: module "%" has no api_name column, so selector "%" cannot name one of its surfaces', + v_ref, resolve_api.selector + USING ERRCODE = 'FR023'; + END IF; + + FOR v_frame IN + SELECT f.lookup_database_id, f.scope, min(f.ord) AS ord + FROM function_resolution.frame_candidates( + resolve_api.database_id, + resolve_api.scope, + resolve_api.entity_id + ) f(lookup_database_id, scope, owner_key, ord) + GROUP BY f.lookup_database_id, f.scope + ORDER BY min(f.ord) + LOOP + -- @scope pins the registration to one scope tier; without it, the + -- frame being walked supplies the scope, so a department execution + -- finds its org's registration when the org frame comes up. + -- pgsql-lint-disable-next-line no-dynamic-sql -- lookup-only: the module registration table is named by the selector + v_query := format( + 'SELECT m.schema_id, m.api_name + FROM metaschema_modules_public.%I m + WHERE m.database_id = $1 + %s + LIMIT 1', + v_module_table, + CASE WHEN v_module_has_scope THEN 'AND m.scope = $2' ELSE 'AND $2 IS NOT NULL' END + ); + + CONTINUE WHEN v_scope_filter IS NOT NULL AND v_scope_filter <> v_frame.scope; + + EXECUTE v_query + INTO v_module_schema_id, v_module_api_name + USING v_frame.lookup_database_id, v_frame.scope; + + CONTINUE WHEN v_module_schema_id IS NULL; + + SELECT r.apis_schema, r.apis_table, r.api_schemas_schema, r.api_schemas_table + INTO v_apis_schema, v_apis_table, v_api_schemas_schema, v_api_schemas_table + FROM app_scope.routing_tables(v_frame.lookup_database_id, v_frame.scope) r; + + CONTINUE WHEN v_apis_schema IS NULL OR v_api_schemas_schema IS NULL; + + -- pgsql-lint-disable-next-line no-dynamic-sql -- lookup-only: the api surface tables are located per frame + v_query := format( + 'SELECT s.api_id AS id, a.name + FROM %I.%I s + JOIN %I.%I a ON a.id = s.api_id + WHERE s.schema_id = $1', + v_api_schemas_schema, v_api_schemas_table, + v_apis_schema, v_apis_table + ); + + v_matches := '[]'::jsonb; + + FOR v_row IN EXECUTE v_query USING v_module_schema_id + LOOP + v_matches := v_matches || to_jsonb(v_row); + END LOOP; + + CONTINUE WHEN jsonb_array_length(v_matches) = 0; + + -- .api names the surface outright; a bare module selector finding + -- several surfaces falls back to the module row's own api_name (the + -- authored intent). Failing both, the selector is genuinely ambiguous + -- and must not guess. + v_match := NULL; + + IF v_api_filter IS NOT NULL THEN + SELECT m INTO v_match + FROM jsonb_array_elements(v_matches) m + WHERE m->>'name' = v_api_filter; + + IF v_match IS NULL THEN + RAISE EXCEPTION 'CAPABILITY_API_NOT_FOUND: module "%" has no attached api named "%" (candidates: %)', + v_ref, + v_api_filter, + (SELECT string_agg(format('%s (%s)', m->>'name', m->>'id'), ', ' ORDER BY m->>'id') + FROM jsonb_array_elements(v_matches) m) + USING ERRCODE = 'FR021'; + END IF; + ELSIF jsonb_array_length(v_matches) > 1 THEN + SELECT m INTO v_match + FROM jsonb_array_elements(v_matches) m + WHERE m->>'name' = v_module_api_name; + + IF v_match IS NULL THEN + RAISE EXCEPTION 'CAPABILITY_API_AMBIGUOUS: module "%" has schemas attached to % apis (candidates: %) and none matches its api_name "%"; name one with %.', + v_ref, + jsonb_array_length(v_matches), + (SELECT string_agg(format('%s (%s)', m->>'name', m->>'id'), ', ' ORDER BY m->>'id') + FROM jsonb_array_elements(v_matches) m), + coalesce(v_module_api_name, ''), + v_ref + USING ERRCODE = 'FR022'; + END IF; + ELSE + v_match := v_matches->0; + END IF; + + SELECT * + INTO v_resolved + FROM function_resolution.api_catalog_row( + resolve_api.database_id, + resolve_api.scope, + resolve_api.entity_id, + (v_match->>'id')::uuid + ); + + -- The attachment names an api the execution cannot reach (another + -- tenant's, or an outer frame's unpublished surface): that is a + -- misconfiguration, not a reason to fall through to a further frame. + IF NOT FOUND THEN + RAISE EXCEPTION 'CAPABILITY_API_UNREACHABLE: selector "%" resolves to api % via module "%", which is not visible to database %', + resolve_api.selector, v_match->>'id', v_ref, resolve_api.database_id + USING ERRCODE = 'FR021'; + END IF; + + resolve_api.api_id := (v_match->>'id')::uuid; + resolve_api.api_name := v_resolved.api_name; + resolve_api.owner_database_id := v_resolved.owner_database_id; + resolve_api.owner_scope := v_resolved.owner_scope; + resolve_api.owner_key := v_resolved.owner_key; + + RETURN NEXT; + RETURN; + END LOOP; + + RAISE EXCEPTION 'CAPABILITY_API_NOT_FOUND: selector "%" resolves no api surface in the scope chain starting at scope "%" (database_id=%): module "%" is either unregistered there%s or has no api attachment', + resolve_api.selector, + resolve_api.scope, + resolve_api.database_id, + v_ref, + CASE WHEN v_scope_filter IS NULL THEN '' ELSE format(' at scope "%s"', v_scope_filter) END + USING ERRCODE = 'FR021'; +END; +$EOFCODE$ LANGUAGE plpgsql STABLE SECURITY DEFINER; + +CREATE FUNCTION function_resolution.bound_bucket_id( + database_id uuid, + scope text, + entity_id uuid, + function_definition_id uuid, + key text +) RETURNS uuid AS $EOFCODE$ +DECLARE + v_bucket_id uuid; +BEGIN + SELECT b.bucket_id + INTO v_bucket_id + FROM function_resolution.frame_candidates( + bound_bucket_id.database_id, + bound_bucket_id.scope, + bound_bucket_id.entity_id + ) cand + JOIN catalog_private.bindings b + ON b.owner_scope = cand.owner_scope + AND b.owner_key IS NOT DISTINCT FROM cand.owner_key + AND b.database_id = CASE + WHEN cand.owner_scope = 'database' THEN cand.owner_key + ELSE cand.lookup_database_id + END + WHERE b.function_id = bound_bucket_id.function_definition_id + AND b.key = bound_bucket_id.key + AND b.bucket_id IS NOT NULL + ORDER BY cand.ord, + CASE b.lifecycle + WHEN 'execution' THEN 0 + WHEN 'root_execution' THEN 1 + ELSE 2 + END + LIMIT 1; + + RETURN v_bucket_id; +END; +$EOFCODE$ LANGUAGE plpgsql STABLE SECURITY DEFINER; + +CREATE FUNCTION function_resolution.resolve_payload_refs( + database_id uuid, + scope text, + entity_id uuid, + payload jsonb +) RETURNS jsonb AS $EOFCODE$ +DECLARE + v_kind text; + v_tags text[]; + v_bucket record; + v_api record; + v_selector text; + v_schema text; + v_name text; + v_table_id uuid; + v_result jsonb; + v_key text; +BEGIN + IF resolve_payload_refs.payload IS NULL THEN + RETURN NULL; + END IF; + + IF jsonb_typeof(resolve_payload_refs.payload) = 'array' THEN + SELECT coalesce(jsonb_agg( + function_resolution.resolve_payload_refs( + resolve_payload_refs.database_id, + resolve_payload_refs.scope, + resolve_payload_refs.entity_id, + elem + ) ORDER BY ord + ), '[]'::jsonb) + INTO v_result + FROM jsonb_array_elements(resolve_payload_refs.payload) WITH ORDINALITY AS t(elem, ord); + + RETURN v_result; + END IF; + + IF jsonb_typeof(resolve_payload_refs.payload) <> 'object' THEN + RETURN resolve_payload_refs.payload; + END IF; + + IF NOT (resolve_payload_refs.payload ? '$ref') THEN + v_result := '{}'::jsonb; + + FOR v_key IN SELECT k FROM jsonb_object_keys(resolve_payload_refs.payload) k + LOOP + v_result := v_result || jsonb_build_object( + v_key, + function_resolution.resolve_payload_refs( + resolve_payload_refs.database_id, + resolve_payload_refs.scope, + resolve_payload_refs.entity_id, + resolve_payload_refs.payload -> v_key + ) + ); + END LOOP; + + RETURN v_result; + END IF; + + v_kind := resolve_payload_refs.payload->>'$ref'; + + IF v_kind = 'bucket' THEN + -- Already resolved (trigger-time stamping, or a second pass). + IF resolve_payload_refs.payload ? 'bucket_id' THEN + RETURN resolve_payload_refs.payload; + END IF; + + IF resolve_payload_refs.payload ? 'tags' THEN + SELECT array_agg(t.tag ORDER BY t.ord) + INTO v_tags + FROM jsonb_array_elements_text(resolve_payload_refs.payload->'tags') + WITH ORDINALITY AS t(tag, ord); + ELSIF resolve_payload_refs.payload ? 'key' THEN + -- A single key is the one-tag selector: the declaration vocabulary + -- and the payload vocabulary stay the same thing. + v_tags := ARRAY[resolve_payload_refs.payload->>'key']; + END IF; + + SELECT * + INTO v_bucket + FROM function_resolution.resolve_bucket( + resolve_payload_refs.database_id, + resolve_payload_refs.scope, + resolve_payload_refs.entity_id, + v_tags, + resolve_payload_refs.payload->>'type' + ); + + RETURN jsonb_build_object( + '$ref', 'bucket', + 'bucket_id', v_bucket.bucket_id, + 'key', v_bucket.bucket_key, + 'type', v_bucket.bucket_type, + 'physical_name', v_bucket.physical_name, + 'database_id', v_bucket.owner_database_id + ); + END IF; + + IF v_kind = 'api' THEN + IF resolve_payload_refs.payload ? 'api_id' THEN + RETURN resolve_payload_refs.payload; + END IF; + + -- Both keys carry the selector vocabulary itself: module is a module + -- name (optionally .api / @scope), name is an api name. + v_selector := coalesce( + resolve_payload_refs.payload->>'module', + resolve_payload_refs.payload->>'name' + ); + + SELECT * + INTO v_api + FROM function_resolution.resolve_api( + resolve_payload_refs.database_id, + resolve_payload_refs.scope, + resolve_payload_refs.entity_id, + v_selector + ); + + RETURN jsonb_build_object( + '$ref', 'api', + 'api_id', v_api.api_id, + 'name', v_api.api_name, + 'database_id', v_api.owner_database_id + ); + END IF; + + IF v_kind = 'table' THEN + IF resolve_payload_refs.payload ? 'table_id' THEN + RETURN resolve_payload_refs.payload; + END IF; + + v_schema := resolve_payload_refs.payload->>'schema'; + v_name := resolve_payload_refs.payload->>'name'; + + IF v_schema IS NULL OR v_name IS NULL THEN + RAISE EXCEPTION 'CAPABILITY_TABLE_REF_INVALID: a table reference needs both schema and name (got %)', + resolve_payload_refs.payload + USING ERRCODE = 'FR031'; + END IF; + + SELECT t.id + INTO v_table_id + FROM metaschema_public.schema s + JOIN metaschema_public."table" t ON (t.schema_id = s.id AND t.database_id = s.database_id) + WHERE s.database_id = resolve_payload_refs.database_id + AND s.schema_name = v_schema + AND t.name = v_name; + + IF NOT FOUND THEN + RAISE EXCEPTION 'CAPABILITY_TABLE_NOT_FOUND: database % has no table %.%', + resolve_payload_refs.database_id, v_schema, v_name + USING ERRCODE = 'FR032'; + END IF; + + RETURN jsonb_build_object( + '$ref', 'table', + 'schema', v_schema, + 'name', v_name, + 'table_id', v_table_id + ); + END IF; + + RAISE EXCEPTION 'CAPABILITY_REF_UNKNOWN: "%" is not a known payload reference kind (known kinds: bucket, table, api)', + v_kind + USING ERRCODE = 'FR030'; +END; +$EOFCODE$ LANGUAGE plpgsql STABLE SECURITY DEFINER; + +CREATE FUNCTION function_resolution.resolve_capabilities( + database_id uuid, + scope text, + entity_id uuid, + function_definition_id uuid, + definition_scope text, + definition_database_id uuid DEFAULT NULL, + payload jsonb DEFAULT '{}'::jsonb, + channel text DEFAULT NULL +) RETURNS jsonb AS $EOFCODE$ +DECLARE + v_defn_database_id uuid; + v_defs_schema text; + v_defs_table text; + v_query text; + v_definition jsonb; + v_access_channels text[]; + v_key text; + v_bound_bucket_id uuid; + -- The keys a tenant fulfilled with an explicit binding, paired positionally + -- with the bucket each binding names: the two resolution routes are disjoint + -- sets of keys, resolved by one query each rather than key by key. + v_bound_keys text[]; + v_bound_ids uuid[]; + v_buckets jsonb := '{}'::jsonb; + v_apis jsonb := '{}'::jsonb; +BEGIN + v_defn_database_id := coalesce( + resolve_capabilities.definition_database_id, + resolve_capabilities.database_id + ); + + SELECT l.schema_name, l.table_name + INTO v_defs_schema, v_defs_table + FROM function_resolution.definitions_location(v_defn_database_id, resolve_capabilities.definition_scope) l; + + IF v_defs_schema IS NULL THEN + RAISE EXCEPTION 'CAPABILITY_DEFINITION_SCOPE_UNPROVISIONED: database % has no function module at scope "%"', + v_defn_database_id, resolve_capabilities.definition_scope + USING ERRCODE = 'FR040'; + END IF; + + -- to_jsonb of the row rather than a column list: the declaration set grows, + -- and a resolver that names columns fails on a database whose function + -- module predates the newest one. + -- pgsql-lint-disable-next-line no-dynamic-sql -- lookup-only: the definitions table is located per scope + v_query := format('SELECT to_jsonb(d) FROM %I.%I d WHERE d.id = $1', v_defs_schema, v_defs_table); + + EXECUTE v_query INTO v_definition USING resolve_capabilities.function_definition_id; + + IF v_definition IS NULL THEN + RAISE EXCEPTION 'CAPABILITY_DEFINITION_NOT_FOUND: no function definition % at scope "%" in database %', + resolve_capabilities.function_definition_id, + resolve_capabilities.definition_scope, + v_defn_database_id + USING ERRCODE = 'FR040'; + END IF; + + -- Lane check: access_channels is who may invoke a function, so an + -- invocation arriving through a channel the definition does not list is + -- refused here rather than at the image, which cannot know. + IF resolve_capabilities.channel IS NOT NULL THEN + SELECT array_agg(c.channel) + INTO v_access_channels + FROM jsonb_array_elements_text(coalesce(v_definition->'access_channels', '[]'::jsonb)) AS c(channel); + + IF NOT coalesce(v_access_channels, ARRAY[]::text[]) @> ARRAY[resolve_capabilities.channel] THEN + RAISE EXCEPTION 'CAPABILITY_CHANNEL_REFUSED: function % does not declare the "%" access channel (declares: %)', + resolve_capabilities.function_definition_id, + resolve_capabilities.channel, + coalesce(array_to_string(v_access_channels, ', '), '') + USING ERRCODE = 'FR041'; + END IF; + END IF; + + -- ========================================================================= + -- required_buckets: explicit binding first, then discovery by tag + -- + -- Set-based, in three statements rather than a loop per key, because every + -- declared key resolves independently: the bindings are read once, the + -- reachability of all of them is proved once, and the remaining keys are + -- resolved by tag once. The two routes are kept in separate statements on + -- purpose — resolve_bucket RAISES when a tag matches nothing, so evaluating + -- it for a bound key (which needs no tag) would turn a valid declaration + -- into an error, and a LEFT JOIN LATERAL's ON clause is no guarantee the + -- function is not evaluated. + -- ========================================================================= + SELECT array_agg(b.key ORDER BY b.ord), array_agg(b.bucket_id ORDER BY b.ord) + INTO v_bound_keys, v_bound_ids + FROM ( + SELECT k.key, + k.ord, + function_resolution.bound_bucket_id( + resolve_capabilities.database_id, + resolve_capabilities.scope, + resolve_capabilities.entity_id, + resolve_capabilities.function_definition_id, + k.key + ) AS bucket_id + FROM jsonb_array_elements_text( + coalesce(v_definition->'required_buckets', '[]'::jsonb) + ) WITH ORDINALITY AS k(key, ord) + ) b + WHERE b.bucket_id IS NOT NULL; + + -- Same-tenant enforcement: a binding naming a bucket outside the execution's + -- own frame chain (or an outer frame's private one) must fail the whole + -- invocation. The generated binding guard cannot check this — compute's + -- published modules may not reference storage — so it is checked here, where + -- a function would otherwise be handed the bucket. + IF v_bound_keys IS NOT NULL THEN + SELECT b.key, b.bucket_id + INTO v_key, v_bound_bucket_id + FROM unnest(v_bound_keys, v_bound_ids) AS b(key, bucket_id) + WHERE NOT EXISTS ( + SELECT 1 + FROM function_resolution.bucket_catalog_row( + resolve_capabilities.database_id, + resolve_capabilities.scope, + resolve_capabilities.entity_id, + b.bucket_id + ) + ) + ORDER BY b.key + LIMIT 1; + + IF FOUND THEN + RAISE EXCEPTION 'CAPABILITY_BINDING_UNREACHABLE: capability "%" of function % is bound to bucket %, which database % may not reach', + v_key, + resolve_capabilities.function_definition_id, + v_bound_bucket_id, + resolve_capabilities.database_id + USING ERRCODE = 'FR013'; + END IF; + + SELECT jsonb_object_agg(b.key, jsonb_build_object( + 'bucket_id', b.bucket_id, + 'key', c.bucket_key, + 'type', c.bucket_type, + 'physical_name', c.physical_name, + 'database_id', c.owner_database_id, + 'source', 'binding' + )) + INTO v_buckets + FROM unnest(v_bound_keys, v_bound_ids) AS b(key, bucket_id) + CROSS JOIN LATERAL function_resolution.bucket_catalog_row( + resolve_capabilities.database_id, + resolve_capabilities.scope, + resolve_capabilities.entity_id, + b.bucket_id + ) c; + END IF; + + -- Discovery by tag, for every declared key the tenant did not bind. The + -- unbound set is a MATERIALIZED CTE so the bound keys are excluded *before* + -- resolve_bucket runs: a bound key needs no tag, and evaluating it would + -- raise CAPABILITY_BUCKET_NOT_FOUND on a perfectly valid declaration. + WITH unbound AS MATERIALIZED ( + SELECT k.key + FROM jsonb_array_elements_text( + coalesce(v_definition->'required_buckets', '[]'::jsonb) + ) AS k(key) + WHERE NOT k.key = ANY(coalesce(v_bound_keys, ARRAY[]::text[])) + ) + SELECT coalesce(v_buckets, '{}'::jsonb) || coalesce(jsonb_object_agg(k.key, jsonb_build_object( + 'bucket_id', r.bucket_id, + 'key', r.bucket_key, + 'type', r.bucket_type, + 'physical_name', r.physical_name, + 'database_id', r.owner_database_id, + 'source', 'tags' + )), '{}'::jsonb) + INTO v_buckets + FROM unbound k + CROSS JOIN LATERAL function_resolution.resolve_bucket( + resolve_capabilities.database_id, + resolve_capabilities.scope, + resolve_capabilities.entity_id, + ARRAY[k.key], + NULL + ) r; + + -- ========================================================================= + -- required_modules: module names, the same vocabulary the presets use + -- ([.][@], or a bare api name as the escape hatch). + -- resolve_api raises on an unresolvable selector, which propagates out of + -- the lateral and fails the invocation — the intended behaviour. + -- ========================================================================= + SELECT coalesce(jsonb_object_agg(s.selector, jsonb_build_object( + 'api_id', a.api_id, + 'name', a.api_name, + 'database_id', a.owner_database_id + )), '{}'::jsonb) + INTO v_apis + FROM jsonb_array_elements_text( + coalesce(v_definition->'required_modules', '[]'::jsonb) + ) AS s(selector) + CROSS JOIN LATERAL function_resolution.resolve_api( + resolve_capabilities.database_id, + resolve_capabilities.scope, + resolve_capabilities.entity_id, + s.selector + ) a; + + RETURN jsonb_build_object( + 'function_definition_id', resolve_capabilities.function_definition_id, + 'definition_scope', resolve_capabilities.definition_scope, + 'definition_database_id', v_defn_database_id, + 'database_id', resolve_capabilities.database_id, + 'scope', resolve_capabilities.scope, + 'entity_id', resolve_capabilities.entity_id, + 'buckets', v_buckets, + 'apis', v_apis, + 'models', coalesce(v_definition->'required_models', '[]'::jsonb), + 'secrets', coalesce(v_definition->'required_secrets', '[]'::jsonb), + 'configs', coalesce(v_definition->'required_configs', '[]'::jsonb), + 'integrations', coalesce(v_definition->'integrations', '[]'::jsonb), + 'access_channels', coalesce(v_definition->'access_channels', '[]'::jsonb), + 'payload', function_resolution.resolve_payload_refs( + resolve_capabilities.database_id, + resolve_capabilities.scope, + resolve_capabilities.entity_id, + coalesce(resolve_capabilities.payload, '{}'::jsonb) + ) + ); +END; +$EOFCODE$ LANGUAGE plpgsql STABLE SECURITY DEFINER; + +CREATE FUNCTION function_resolution.validate_capabilities( + database_id uuid, + scope text, + entity_id uuid, + function_definition_id uuid, + definition_scope text, + definition_database_id uuid DEFAULT NULL, + payload jsonb DEFAULT '{}'::jsonb, + channel text DEFAULT NULL +) RETURNS void AS $EOFCODE$ +BEGIN + PERFORM function_resolution.resolve_capabilities( + validate_capabilities.database_id, + validate_capabilities.scope, + validate_capabilities.entity_id, + validate_capabilities.function_definition_id, + validate_capabilities.definition_scope, + validate_capabilities.definition_database_id, + validate_capabilities.payload, + validate_capabilities.channel + ); +END; +$EOFCODE$ LANGUAGE plpgsql STABLE SECURITY DEFINER; \ No newline at end of file diff --git a/packages/function-resolution/verify/schemas/function_resolution/procedures/api_catalog_row.sql b/packages/function-resolution/verify/schemas/function_resolution/procedures/api_catalog_row.sql new file mode 100644 index 000000000..7b27a3080 --- /dev/null +++ b/packages/function-resolution/verify/schemas/function_resolution/procedures/api_catalog_row.sql @@ -0,0 +1,7 @@ +-- Verify schemas/function_resolution/procedures/api_catalog_row on pg + +BEGIN; + +SELECT assert_function('function_resolution.api_catalog_row(uuid, text, uuid, uuid)'::regprocedure); + +ROLLBACK; diff --git a/packages/function-resolution/verify/schemas/function_resolution/procedures/bound_bucket_id.sql b/packages/function-resolution/verify/schemas/function_resolution/procedures/bound_bucket_id.sql new file mode 100644 index 000000000..12062e422 --- /dev/null +++ b/packages/function-resolution/verify/schemas/function_resolution/procedures/bound_bucket_id.sql @@ -0,0 +1,7 @@ +-- Verify schemas/function_resolution/procedures/bound_bucket_id on pg + +BEGIN; + +SELECT assert_function('function_resolution.bound_bucket_id(uuid, text, uuid, uuid, text)'::regprocedure); + +ROLLBACK; diff --git a/packages/function-resolution/verify/schemas/function_resolution/procedures/bucket_catalog_row.sql b/packages/function-resolution/verify/schemas/function_resolution/procedures/bucket_catalog_row.sql new file mode 100644 index 000000000..9a18c9b3f --- /dev/null +++ b/packages/function-resolution/verify/schemas/function_resolution/procedures/bucket_catalog_row.sql @@ -0,0 +1,7 @@ +-- Verify schemas/function_resolution/procedures/bucket_catalog_row on pg + +BEGIN; + +SELECT assert_function('function_resolution.bucket_catalog_row(uuid, text, uuid, uuid)'::regprocedure); + +ROLLBACK; diff --git a/packages/function-resolution/verify/schemas/function_resolution/procedures/catalog_location.sql b/packages/function-resolution/verify/schemas/function_resolution/procedures/catalog_location.sql deleted file mode 100644 index 20c7c35a0..000000000 --- a/packages/function-resolution/verify/schemas/function_resolution/procedures/catalog_location.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Verify schemas/function_resolution/procedures/catalog_location on pg - -BEGIN; - -SELECT verify_function ('function_resolution.catalog_location'); - -ROLLBACK; diff --git a/packages/function-resolution/verify/schemas/function_resolution/procedures/definitions_location.sql b/packages/function-resolution/verify/schemas/function_resolution/procedures/definitions_location.sql index 6389d0fbf..b950bc4c1 100644 --- a/packages/function-resolution/verify/schemas/function_resolution/procedures/definitions_location.sql +++ b/packages/function-resolution/verify/schemas/function_resolution/procedures/definitions_location.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('function_resolution.definitions_location'); +SELECT assert_function('function_resolution.definitions_location(uuid, text)'::regprocedure); ROLLBACK; diff --git a/packages/function-resolution/verify/schemas/function_resolution/procedures/enqueue.sql b/packages/function-resolution/verify/schemas/function_resolution/procedures/enqueue.sql index bfc007de8..46f383df3 100644 --- a/packages/function-resolution/verify/schemas/function_resolution/procedures/enqueue.sql +++ b/packages/function-resolution/verify/schemas/function_resolution/procedures/enqueue.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('function_resolution.enqueue'); +SELECT assert_function('function_resolution.enqueue(text, json, text, uuid, uuid, text, text, text, timestamptz, int4, int4, uuid, text, bool, text, uuid, uuid)'::regprocedure); ROLLBACK; diff --git a/packages/function-resolution/verify/schemas/function_resolution/procedures/frame_candidates.sql b/packages/function-resolution/verify/schemas/function_resolution/procedures/frame_candidates.sql new file mode 100644 index 000000000..c24c9ba35 --- /dev/null +++ b/packages/function-resolution/verify/schemas/function_resolution/procedures/frame_candidates.sql @@ -0,0 +1,7 @@ +-- Verify schemas/function_resolution/procedures/frame_candidates on pg + +BEGIN; + +SELECT assert_function('function_resolution.frame_candidates(uuid, text, uuid)'::regprocedure); + +ROLLBACK; diff --git a/packages/function-resolution/verify/schemas/function_resolution/procedures/resolve.sql b/packages/function-resolution/verify/schemas/function_resolution/procedures/resolve.sql index b0c9647c4..864d419a2 100644 --- a/packages/function-resolution/verify/schemas/function_resolution/procedures/resolve.sql +++ b/packages/function-resolution/verify/schemas/function_resolution/procedures/resolve.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('function_resolution.resolve'); +SELECT assert_function('function_resolution.resolve(uuid, text, uuid, text, bool)'::regprocedure); ROLLBACK; diff --git a/packages/function-resolution/verify/schemas/function_resolution/procedures/resolve_api.sql b/packages/function-resolution/verify/schemas/function_resolution/procedures/resolve_api.sql new file mode 100644 index 000000000..86f54c739 --- /dev/null +++ b/packages/function-resolution/verify/schemas/function_resolution/procedures/resolve_api.sql @@ -0,0 +1,7 @@ +-- Verify schemas/function_resolution/procedures/resolve_api on pg + +BEGIN; + +SELECT assert_function('function_resolution.resolve_api(uuid, text, uuid, text)'::regprocedure); + +ROLLBACK; diff --git a/packages/function-resolution/verify/schemas/function_resolution/procedures/resolve_bucket.sql b/packages/function-resolution/verify/schemas/function_resolution/procedures/resolve_bucket.sql new file mode 100644 index 000000000..f57a071f3 --- /dev/null +++ b/packages/function-resolution/verify/schemas/function_resolution/procedures/resolve_bucket.sql @@ -0,0 +1,7 @@ +-- Verify schemas/function_resolution/procedures/resolve_bucket on pg + +BEGIN; + +SELECT assert_function('function_resolution.resolve_bucket(uuid, text, uuid, text[], text)'::regprocedure); + +ROLLBACK; diff --git a/packages/function-resolution/verify/schemas/function_resolution/procedures/resolve_capabilities.sql b/packages/function-resolution/verify/schemas/function_resolution/procedures/resolve_capabilities.sql new file mode 100644 index 000000000..0e6fe50be --- /dev/null +++ b/packages/function-resolution/verify/schemas/function_resolution/procedures/resolve_capabilities.sql @@ -0,0 +1,7 @@ +-- Verify schemas/function_resolution/procedures/resolve_capabilities on pg + +BEGIN; + +SELECT assert_function('function_resolution.resolve_capabilities(uuid, text, uuid, uuid, text, uuid, jsonb, text)'::regprocedure); + +ROLLBACK; diff --git a/packages/function-resolution/verify/schemas/function_resolution/procedures/resolve_invocation.sql b/packages/function-resolution/verify/schemas/function_resolution/procedures/resolve_invocation.sql index b4b4e941b..02702667d 100644 --- a/packages/function-resolution/verify/schemas/function_resolution/procedures/resolve_invocation.sql +++ b/packages/function-resolution/verify/schemas/function_resolution/procedures/resolve_invocation.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('function_resolution.resolve_invocation'); +SELECT assert_function('function_resolution.resolve_invocation(uuid, text, uuid, text, uuid, text, uuid)'::regprocedure); ROLLBACK; diff --git a/packages/function-resolution/verify/schemas/function_resolution/procedures/resolve_payload_refs.sql b/packages/function-resolution/verify/schemas/function_resolution/procedures/resolve_payload_refs.sql new file mode 100644 index 000000000..2cc79f5ba --- /dev/null +++ b/packages/function-resolution/verify/schemas/function_resolution/procedures/resolve_payload_refs.sql @@ -0,0 +1,7 @@ +-- Verify schemas/function_resolution/procedures/resolve_payload_refs on pg + +BEGIN; + +SELECT assert_function('function_resolution.resolve_payload_refs(uuid, text, uuid, jsonb)'::regprocedure); + +ROLLBACK; diff --git a/packages/function-resolution/verify/schemas/function_resolution/procedures/routing.sql b/packages/function-resolution/verify/schemas/function_resolution/procedures/routing.sql index d2109fded..850ae4365 100644 --- a/packages/function-resolution/verify/schemas/function_resolution/procedures/routing.sql +++ b/packages/function-resolution/verify/schemas/function_resolution/procedures/routing.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('function_resolution.routing'); +SELECT assert_function('function_resolution.routing(uuid, uuid)'::regprocedure); ROLLBACK; diff --git a/packages/function-resolution/verify/schemas/function_resolution/procedures/validate_capabilities.sql b/packages/function-resolution/verify/schemas/function_resolution/procedures/validate_capabilities.sql new file mode 100644 index 000000000..772cad731 --- /dev/null +++ b/packages/function-resolution/verify/schemas/function_resolution/procedures/validate_capabilities.sql @@ -0,0 +1,7 @@ +-- Verify schemas/function_resolution/procedures/validate_capabilities on pg + +BEGIN; + +SELECT assert_function('function_resolution.validate_capabilities(uuid, text, uuid, uuid, text, uuid, jsonb, text)'::regprocedure); + +ROLLBACK; diff --git a/packages/function-resolution/verify/schemas/function_resolution/schema.sql b/packages/function-resolution/verify/schemas/function_resolution/schema.sql index c2353963b..de8ebb50a 100644 --- a/packages/function-resolution/verify/schemas/function_resolution/schema.sql +++ b/packages/function-resolution/verify/schemas/function_resolution/schema.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_schema ('function_resolution'); +SELECT assert_schema('function_resolution'::regnamespace); ROLLBACK; diff --git a/packages/geotypes/Makefile b/packages/geotypes/Makefile index 5ccf2f8cb..0283ddb2b 100644 --- a/packages/geotypes/Makefile +++ b/packages/geotypes/Makefile @@ -1,5 +1,5 @@ EXTENSION = pgpm-geo-types -DATA = sql/pgpm-geo-types--0.36.0.sql +DATA = sql/pgpm-geo-types--0.39.0.sql PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) diff --git a/packages/geotypes/package.json b/packages/geotypes/package.json index fb9f11acd..285f11614 100644 --- a/packages/geotypes/package.json +++ b/packages/geotypes/package.json @@ -1,6 +1,6 @@ { "name": "@pgpm/geotypes", - "version": "0.36.0", + "version": "0.39.0", "description": "Geographic data types and spatial functions for PostgreSQL", "author": "Dan Lynch ", "contributors": [ diff --git a/packages/geotypes/pgpm-geo-types.control b/packages/geotypes/pgpm-geo-types.control index e51059c04..e803941b2 100644 --- a/packages/geotypes/pgpm-geo-types.control +++ b/packages/geotypes/pgpm-geo-types.control @@ -1,6 +1,6 @@ # pgpm-geo-types extension comment = 'pgpm-geo-types extension' -default_version = '0.36.0' +default_version = '0.39.0' module_pathname = '$libdir/pgpm-geo-types' requires = 'plpgsql,citext,postgis,pgpm-types,pgpm-verify' relocatable = false diff --git a/packages/geotypes/sql/pgpm-geo-types--0.36.0.bundle.tar.gz b/packages/geotypes/sql/pgpm-geo-types--0.36.0.bundle.tar.gz deleted file mode 100644 index ed91087a4..000000000 Binary files a/packages/geotypes/sql/pgpm-geo-types--0.36.0.bundle.tar.gz and /dev/null differ diff --git a/packages/geotypes/sql/pgpm-geo-types--0.39.0.bundle.tar.gz b/packages/geotypes/sql/pgpm-geo-types--0.39.0.bundle.tar.gz new file mode 100644 index 000000000..c4d10f3fa Binary files /dev/null and b/packages/geotypes/sql/pgpm-geo-types--0.39.0.bundle.tar.gz differ diff --git a/packages/geotypes/sql/pgpm-geo-types--0.36.0.sql b/packages/geotypes/sql/pgpm-geo-types--0.39.0.sql similarity index 100% rename from packages/geotypes/sql/pgpm-geo-types--0.36.0.sql rename to packages/geotypes/sql/pgpm-geo-types--0.39.0.sql diff --git a/packages/geotypes/verify/schemas/public/domains/geo_point.sql b/packages/geotypes/verify/schemas/public/domains/geo_point.sql index bb8189d5d..4724cf37f 100644 --- a/packages/geotypes/verify/schemas/public/domains/geo_point.sql +++ b/packages/geotypes/verify/schemas/public/domains/geo_point.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_domain ('public.geo_point'); +SELECT assert_domain('public.geo_point'::regtype, 'geometry'::regtype, _constraints => 0); ROLLBACK; diff --git a/packages/geotypes/verify/schemas/public/domains/geo_polygon.sql b/packages/geotypes/verify/schemas/public/domains/geo_polygon.sql index 5a2b085fb..d5033a7c2 100644 --- a/packages/geotypes/verify/schemas/public/domains/geo_polygon.sql +++ b/packages/geotypes/verify/schemas/public/domains/geo_polygon.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_domain ('public.geo_polygon'); +SELECT assert_domain('public.geo_polygon'::regtype, 'geometry'::regtype, _constraints => 0); ROLLBACK; diff --git a/packages/geotypes/verify/schemas/public/domains/geography_point.sql b/packages/geotypes/verify/schemas/public/domains/geography_point.sql index b0666fde0..9dd94b984 100644 --- a/packages/geotypes/verify/schemas/public/domains/geography_point.sql +++ b/packages/geotypes/verify/schemas/public/domains/geography_point.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_domain ('public.geography_point'); +SELECT assert_domain('public.geography_point'::regtype, 'geography'::regtype, _constraints => 0); ROLLBACK; diff --git a/packages/geotypes/verify/schemas/public/domains/geography_polygon.sql b/packages/geotypes/verify/schemas/public/domains/geography_polygon.sql index fad796303..86fa1979f 100644 --- a/packages/geotypes/verify/schemas/public/domains/geography_polygon.sql +++ b/packages/geotypes/verify/schemas/public/domains/geography_polygon.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_domain ('public.geography_polygon'); +SELECT assert_domain('public.geography_polygon'::regtype, 'geography'::regtype, _constraints => 0); ROLLBACK; diff --git a/packages/inflection-db/Makefile b/packages/inflection-db/Makefile index 360d41a4d..e9c23ad0e 100644 --- a/packages/inflection-db/Makefile +++ b/packages/inflection-db/Makefile @@ -1,5 +1,5 @@ EXTENSION = inflection-db -DATA = sql/inflection-db--0.36.0.sql +DATA = sql/inflection-db--0.39.0.sql PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) diff --git a/packages/inflection-db/__tests__/inflection_db/inflection_db.test.ts b/packages/inflection-db/__tests__/inflection_db/inflection_db.test.ts index 2ed83b12f..251edbaa1 100644 --- a/packages/inflection-db/__tests__/inflection_db/inflection_db.test.ts +++ b/packages/inflection-db/__tests__/inflection_db/inflection_db.test.ts @@ -1,7 +1,6 @@ jest.setTimeout(30000); import { getConnections, PgTestClient } from 'pgsql-test'; -// @ts-expect-error - jest-in-case doesn't have type definitions import cases from 'jest-in-case'; let pg: PgTestClient; diff --git a/packages/inflection-db/inflection-db.control b/packages/inflection-db/inflection-db.control index 1c45fd695..7ecc54aba 100644 --- a/packages/inflection-db/inflection-db.control +++ b/packages/inflection-db/inflection-db.control @@ -1,6 +1,6 @@ # inflection-db extension comment = 'inflection-db extension' -default_version = '0.36.0' +default_version = '0.39.0' module_pathname = '$libdir/inflection-db' requires = 'pgpm-inflection,pgpm-verify,plpgsql' relocatable = false diff --git a/packages/inflection-db/jest.config.js b/packages/inflection-db/jest.config.js index 32bd72df9..1e4f905f3 100644 --- a/packages/inflection-db/jest.config.js +++ b/packages/inflection-db/jest.config.js @@ -8,7 +8,6 @@ module.exports = { "ts-jest", { babelConfig: false, - tsconfig: "tsconfig.json", }, ], }, diff --git a/packages/inflection-db/package.json b/packages/inflection-db/package.json index cf38c0d06..31e1695e2 100644 --- a/packages/inflection-db/package.json +++ b/packages/inflection-db/package.json @@ -1,6 +1,6 @@ { "name": "@pgpm/inflection-db", - "version": "0.36.0", + "version": "0.39.0", "author": "Dan Lynch ", "contributors": [ "Constructive " diff --git a/packages/inflection-db/pgpm.plan b/packages/inflection-db/pgpm.plan index 00e1b3aa0..754090992 100644 --- a/packages/inflection-db/pgpm.plan +++ b/packages/inflection-db/pgpm.plan @@ -2,7 +2,7 @@ %project=inflection-db %uri=inflection-db -schemas/inflection_db/schema [pgpm-inflection:schemas/inflection/tables/inflection_rules/indexes/inflection_rules_type_idx pgpm-verify:procedures/verify_view] 2017-08-11T08:11:51Z skitch # add schemas/inflection_db/schema +schemas/inflection_db/schema [pgpm-inflection:schemas/inflection/tables/inflection_rules/indexes/inflection_rules_type_idx pgpm-verify:procedures/assert_view] 2017-08-11T08:11:51Z skitch # add schemas/inflection_db/schema schemas/inflection_db/procedures/get_identifier [schemas/inflection_db/schema] 2017-08-11T08:11:51Z skitch # add schemas/inflection_db/procedures/get_identifier schemas/inflection_db/procedures/get_table_name [schemas/inflection_db/schema schemas/inflection_db/procedures/get_identifier] 2017-08-11T08:11:51Z skitch # add schemas/inflection_db/procedures/get_table_name schemas/inflection_db/procedures/get_check_constraint_name [schemas/inflection_db/schema schemas/inflection_db/procedures/get_table_name schemas/inflection_db/procedures/get_identifier] 2017-08-11T08:11:51Z skitch # add schemas/inflection_db/procedures/get_check_constraint_name diff --git a/packages/inflection-db/revert/schemas/inflection_db/procedures/get_check_constraint_name.sql b/packages/inflection-db/revert/schemas/inflection_db/procedures/get_check_constraint_name.sql index 3b51407ac..65ec0384a 100644 --- a/packages/inflection-db/revert/schemas/inflection_db/procedures/get_check_constraint_name.sql +++ b/packages/inflection-db/revert/schemas/inflection_db/procedures/get_check_constraint_name.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION inflection_db.get_check_constraint_name; +DROP FUNCTION inflection_db.get_check_constraint_name(text, text[]); COMMIT; diff --git a/packages/inflection-db/revert/schemas/inflection_db/procedures/get_field_name.sql b/packages/inflection-db/revert/schemas/inflection_db/procedures/get_field_name.sql index 2cbbc3523..d62f11a43 100644 --- a/packages/inflection-db/revert/schemas/inflection_db/procedures/get_field_name.sql +++ b/packages/inflection-db/revert/schemas/inflection_db/procedures/get_field_name.sql @@ -2,7 +2,7 @@ BEGIN; -DROP FUNCTION inflection_db.get_field_name (text); -DROP FUNCTION inflection_db.get_field_name (text[]); +DROP FUNCTION inflection_db.get_field_name(text[]); +DROP FUNCTION inflection_db.get_field_name(text); COMMIT; diff --git a/packages/inflection-db/revert/schemas/inflection_db/procedures/get_foreign_key_field_name.sql b/packages/inflection-db/revert/schemas/inflection_db/procedures/get_foreign_key_field_name.sql index 6386df4d8..f03800f53 100644 --- a/packages/inflection-db/revert/schemas/inflection_db/procedures/get_foreign_key_field_name.sql +++ b/packages/inflection-db/revert/schemas/inflection_db/procedures/get_foreign_key_field_name.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION inflection_db.get_foreign_key_field_name; +DROP FUNCTION inflection_db.get_foreign_key_field_name(text); COMMIT; diff --git a/packages/inflection-db/revert/schemas/inflection_db/procedures/get_foreign_key_index_name.sql b/packages/inflection-db/revert/schemas/inflection_db/procedures/get_foreign_key_index_name.sql index fcef2174f..ea4b521ad 100644 --- a/packages/inflection-db/revert/schemas/inflection_db/procedures/get_foreign_key_index_name.sql +++ b/packages/inflection-db/revert/schemas/inflection_db/procedures/get_foreign_key_index_name.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION inflection_db.get_foreign_key_index_name; +DROP FUNCTION inflection_db.get_foreign_key_index_name(text, text); COMMIT; diff --git a/packages/inflection-db/revert/schemas/inflection_db/procedures/get_identifier.sql b/packages/inflection-db/revert/schemas/inflection_db/procedures/get_identifier.sql index 917c004d5..0f00cd01b 100644 --- a/packages/inflection-db/revert/schemas/inflection_db/procedures/get_identifier.sql +++ b/packages/inflection-db/revert/schemas/inflection_db/procedures/get_identifier.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION inflection_db.get_identifier; +DROP FUNCTION inflection_db.get_identifier(text); COMMIT; diff --git a/packages/inflection-db/revert/schemas/inflection_db/procedures/get_identifier_name.sql b/packages/inflection-db/revert/schemas/inflection_db/procedures/get_identifier_name.sql index ee5f3d192..586ee7abe 100644 --- a/packages/inflection-db/revert/schemas/inflection_db/procedures/get_identifier_name.sql +++ b/packages/inflection-db/revert/schemas/inflection_db/procedures/get_identifier_name.sql @@ -2,7 +2,7 @@ BEGIN; -DROP FUNCTION inflection_db.get_identifier_name (text); -DROP FUNCTION inflection_db.get_identifier_name (text[]); +DROP FUNCTION inflection_db.get_identifier_name(text[]); +DROP FUNCTION inflection_db.get_identifier_name(text); COMMIT; diff --git a/packages/inflection-db/revert/schemas/inflection_db/procedures/get_index_name.sql b/packages/inflection-db/revert/schemas/inflection_db/procedures/get_index_name.sql index fdd9ce840..1c454394e 100644 --- a/packages/inflection-db/revert/schemas/inflection_db/procedures/get_index_name.sql +++ b/packages/inflection-db/revert/schemas/inflection_db/procedures/get_index_name.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION inflection_db.get_index_name; +DROP FUNCTION inflection_db.get_index_name(text, text[]); COMMIT; diff --git a/packages/inflection-db/revert/schemas/inflection_db/procedures/get_namespace_name.sql b/packages/inflection-db/revert/schemas/inflection_db/procedures/get_namespace_name.sql index 73afa6074..b7a8df29c 100644 --- a/packages/inflection-db/revert/schemas/inflection_db/procedures/get_namespace_name.sql +++ b/packages/inflection-db/revert/schemas/inflection_db/procedures/get_namespace_name.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION IF EXISTS inflection_db.get_namespace_name (text[]); +DROP FUNCTION inflection_db.get_namespace_name(text[]); COMMIT; diff --git a/packages/inflection-db/revert/schemas/inflection_db/procedures/get_primary_key_index_name.sql b/packages/inflection-db/revert/schemas/inflection_db/procedures/get_primary_key_index_name.sql index c8d4bc896..2eaa3257d 100644 --- a/packages/inflection-db/revert/schemas/inflection_db/procedures/get_primary_key_index_name.sql +++ b/packages/inflection-db/revert/schemas/inflection_db/procedures/get_primary_key_index_name.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION inflection_db.get_primary_key_index_name; +DROP FUNCTION inflection_db.get_primary_key_index_name(text); COMMIT; diff --git a/packages/inflection-db/revert/schemas/inflection_db/procedures/get_schema_name.sql b/packages/inflection-db/revert/schemas/inflection_db/procedures/get_schema_name.sql index 03a543f37..09f35be31 100644 --- a/packages/inflection-db/revert/schemas/inflection_db/procedures/get_schema_name.sql +++ b/packages/inflection-db/revert/schemas/inflection_db/procedures/get_schema_name.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION inflection_db.get_schema_name; +DROP FUNCTION inflection_db.get_schema_name(text[]); COMMIT; diff --git a/packages/inflection-db/revert/schemas/inflection_db/procedures/get_table_name.sql b/packages/inflection-db/revert/schemas/inflection_db/procedures/get_table_name.sql index 3f93aa851..54b9f3bef 100644 --- a/packages/inflection-db/revert/schemas/inflection_db/procedures/get_table_name.sql +++ b/packages/inflection-db/revert/schemas/inflection_db/procedures/get_table_name.sql @@ -2,7 +2,7 @@ BEGIN; -DROP FUNCTION inflection_db.get_table_name (text); -DROP FUNCTION inflection_db.get_table_name (text[]); +DROP FUNCTION inflection_db.get_table_name(text[]); +DROP FUNCTION inflection_db.get_table_name(text); COMMIT; diff --git a/packages/inflection-db/revert/schemas/inflection_db/procedures/get_table_plural_name.sql b/packages/inflection-db/revert/schemas/inflection_db/procedures/get_table_plural_name.sql index 79a95b362..5128893b1 100644 --- a/packages/inflection-db/revert/schemas/inflection_db/procedures/get_table_plural_name.sql +++ b/packages/inflection-db/revert/schemas/inflection_db/procedures/get_table_plural_name.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION inflection_db.get_table_plural_name; +DROP FUNCTION inflection_db.get_table_plural_name(text); COMMIT; diff --git a/packages/inflection-db/revert/schemas/inflection_db/procedures/get_table_singular_name.sql b/packages/inflection-db/revert/schemas/inflection_db/procedures/get_table_singular_name.sql index 6db23882b..17b8eb809 100644 --- a/packages/inflection-db/revert/schemas/inflection_db/procedures/get_table_singular_name.sql +++ b/packages/inflection-db/revert/schemas/inflection_db/procedures/get_table_singular_name.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION inflection_db.get_table_singular_name; +DROP FUNCTION inflection_db.get_table_singular_name(text); COMMIT; diff --git a/packages/inflection-db/revert/schemas/inflection_db/procedures/get_unique_index_name.sql b/packages/inflection-db/revert/schemas/inflection_db/procedures/get_unique_index_name.sql index ae82c4857..c246656bb 100644 --- a/packages/inflection-db/revert/schemas/inflection_db/procedures/get_unique_index_name.sql +++ b/packages/inflection-db/revert/schemas/inflection_db/procedures/get_unique_index_name.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION inflection_db.get_unique_index_name; +DROP FUNCTION inflection_db.get_unique_index_name(text, text[]); COMMIT; diff --git a/packages/inflection-db/sql/inflection-db--0.36.0.bundle.tar.gz b/packages/inflection-db/sql/inflection-db--0.36.0.bundle.tar.gz deleted file mode 100644 index 31a5ec243..000000000 Binary files a/packages/inflection-db/sql/inflection-db--0.36.0.bundle.tar.gz and /dev/null differ diff --git a/packages/inflection-db/sql/inflection-db--0.39.0.bundle.tar.gz b/packages/inflection-db/sql/inflection-db--0.39.0.bundle.tar.gz new file mode 100644 index 000000000..ade98d2d8 Binary files /dev/null and b/packages/inflection-db/sql/inflection-db--0.39.0.bundle.tar.gz differ diff --git a/packages/inflection-db/sql/inflection-db--0.36.0.sql b/packages/inflection-db/sql/inflection-db--0.39.0.sql similarity index 100% rename from packages/inflection-db/sql/inflection-db--0.36.0.sql rename to packages/inflection-db/sql/inflection-db--0.39.0.sql diff --git a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_check_constraint_name.sql b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_check_constraint_name.sql index 5cfcfbf8c..a87585fd3 100644 --- a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_check_constraint_name.sql +++ b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_check_constraint_name.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('inflection_db.get_check_constraint_name'); +SELECT assert_function('inflection_db.get_check_constraint_name(text, text[])'::regprocedure); ROLLBACK; diff --git a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_field_name.sql b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_field_name.sql index 8f927a91a..44f5d6640 100644 --- a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_field_name.sql +++ b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_field_name.sql @@ -2,6 +2,7 @@ BEGIN; -SELECT verify_function ('inflection_db.get_field_name'); +SELECT assert_function('inflection_db.get_field_name(text)'::regprocedure); +SELECT assert_function('inflection_db.get_field_name(text[])'::regprocedure); ROLLBACK; diff --git a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_foreign_key_field_name.sql b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_foreign_key_field_name.sql index 0dd1c6b39..8e62e162a 100644 --- a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_foreign_key_field_name.sql +++ b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_foreign_key_field_name.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('inflection_db.get_foreign_key_field_name'); +SELECT assert_function('inflection_db.get_foreign_key_field_name(text)'::regprocedure); ROLLBACK; diff --git a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_foreign_key_index_name.sql b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_foreign_key_index_name.sql index dbdf08bff..f02057acb 100644 --- a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_foreign_key_index_name.sql +++ b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_foreign_key_index_name.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('inflection_db.get_foreign_key_index_name'); +SELECT assert_function('inflection_db.get_foreign_key_index_name(text, text)'::regprocedure); ROLLBACK; diff --git a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_identifier.sql b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_identifier.sql index 8c7792439..c9514f19e 100644 --- a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_identifier.sql +++ b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_identifier.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('inflection_db.get_identifier'); +SELECT assert_function('inflection_db.get_identifier(text)'::regprocedure); ROLLBACK; diff --git a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_identifier_name.sql b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_identifier_name.sql index 92e85845d..80d85c6cd 100644 --- a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_identifier_name.sql +++ b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_identifier_name.sql @@ -2,6 +2,7 @@ BEGIN; -SELECT verify_function ('inflection_db.get_identifier_name'); +SELECT assert_function('inflection_db.get_identifier_name(text)'::regprocedure); +SELECT assert_function('inflection_db.get_identifier_name(text[])'::regprocedure); ROLLBACK; diff --git a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_index_name.sql b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_index_name.sql index feccab1d2..b554ff656 100644 --- a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_index_name.sql +++ b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_index_name.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('inflection_db.get_index_name'); +SELECT assert_function('inflection_db.get_index_name(text, text[])'::regprocedure); ROLLBACK; diff --git a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_namespace_name.sql b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_namespace_name.sql index 79876b95a..9582dc0df 100644 --- a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_namespace_name.sql +++ b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_namespace_name.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT inflection_db.get_namespace_name(ARRAY['test']::text[]); +SELECT assert_function('inflection_db.get_namespace_name(text[])'::regprocedure); ROLLBACK; diff --git a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_primary_key_index_name.sql b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_primary_key_index_name.sql index 315c8d0d9..e251fd984 100644 --- a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_primary_key_index_name.sql +++ b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_primary_key_index_name.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('inflection_db.get_primary_key_index_name'); +SELECT assert_function('inflection_db.get_primary_key_index_name(text)'::regprocedure); ROLLBACK; diff --git a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_schema_name.sql b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_schema_name.sql index 7751df555..7633c3968 100644 --- a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_schema_name.sql +++ b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_schema_name.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('inflection_db.get_schema_name'); +SELECT assert_function('inflection_db.get_schema_name(text[])'::regprocedure); ROLLBACK; diff --git a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_table_name.sql b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_table_name.sql index 1c334b8fd..7e1fb6b50 100644 --- a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_table_name.sql +++ b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_table_name.sql @@ -2,6 +2,7 @@ BEGIN; -SELECT verify_function ('inflection_db.get_table_name'); +SELECT assert_function('inflection_db.get_table_name(text)'::regprocedure); +SELECT assert_function('inflection_db.get_table_name(text[])'::regprocedure); ROLLBACK; diff --git a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_table_plural_name.sql b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_table_plural_name.sql index b497b6166..ed60b03f9 100644 --- a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_table_plural_name.sql +++ b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_table_plural_name.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('inflection_db.get_table_plural_name'); +SELECT assert_function('inflection_db.get_table_plural_name(text)'::regprocedure); ROLLBACK; diff --git a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_table_singular_name.sql b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_table_singular_name.sql index 1d0d64bc3..f0f8028ca 100644 --- a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_table_singular_name.sql +++ b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_table_singular_name.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('inflection_db.get_table_singular_name'); +SELECT assert_function('inflection_db.get_table_singular_name(text)'::regprocedure); ROLLBACK; diff --git a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_unique_index_name.sql b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_unique_index_name.sql index a9b3df688..6dae3499b 100644 --- a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_unique_index_name.sql +++ b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_unique_index_name.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('inflection_db.get_unique_index_name'); +SELECT assert_function('inflection_db.get_unique_index_name(text, text[])'::regprocedure); ROLLBACK; diff --git a/packages/inflection-db/verify/schemas/inflection_db/schema.sql b/packages/inflection-db/verify/schemas/inflection_db/schema.sql index 77a5645dd..1a6e9a131 100644 --- a/packages/inflection-db/verify/schemas/inflection_db/schema.sql +++ b/packages/inflection-db/verify/schemas/inflection_db/schema.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_schema ('inflection_db'); +SELECT assert_schema('inflection_db'::regnamespace); ROLLBACK; diff --git a/packages/inflection/Makefile b/packages/inflection/Makefile index 898f813ad..480ca922d 100644 --- a/packages/inflection/Makefile +++ b/packages/inflection/Makefile @@ -1,5 +1,5 @@ EXTENSION = pgpm-inflection -DATA = sql/pgpm-inflection--0.36.0.sql +DATA = sql/pgpm-inflection--0.39.0.sql PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) diff --git a/packages/inflection/package.json b/packages/inflection/package.json index 4d394e826..87aa4eb5f 100644 --- a/packages/inflection/package.json +++ b/packages/inflection/package.json @@ -1,6 +1,6 @@ { "name": "@pgpm/inflection", - "version": "0.36.0", + "version": "0.39.0", "description": "String inflection utilities for PostgreSQL naming conventions", "author": "Dan Lynch ", "contributors": [ diff --git a/packages/inflection/pgpm-inflection.control b/packages/inflection/pgpm-inflection.control index 69e1857b9..dc4fc8d3f 100644 --- a/packages/inflection/pgpm-inflection.control +++ b/packages/inflection/pgpm-inflection.control @@ -1,6 +1,6 @@ # pgpm-inflection extension comment = 'pgpm-inflection extension' -default_version = '0.36.0' +default_version = '0.39.0' module_pathname = '$libdir/pgpm-inflection' requires = 'plpgsql,unaccent,pgpm-verify' relocatable = false diff --git a/packages/inflection/revert/schemas/inflection/procedures/camel.sql b/packages/inflection/revert/schemas/inflection/procedures/camel.sql index 7356e0f76..16b39a94a 100644 --- a/packages/inflection/revert/schemas/inflection/procedures/camel.sql +++ b/packages/inflection/revert/schemas/inflection/procedures/camel.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION inflection.camel; +DROP FUNCTION inflection.camel(text); COMMIT; diff --git a/packages/inflection/revert/schemas/inflection/procedures/dashed.sql b/packages/inflection/revert/schemas/inflection/procedures/dashed.sql index d822eafa0..dc5f7b562 100644 --- a/packages/inflection/revert/schemas/inflection/procedures/dashed.sql +++ b/packages/inflection/revert/schemas/inflection/procedures/dashed.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION inflection.dashed; +DROP FUNCTION inflection.dashed(text); COMMIT; diff --git a/packages/inflection/revert/schemas/inflection/procedures/lower.sql b/packages/inflection/revert/schemas/inflection/procedures/lower.sql index 47c0f9dd3..a0ece0d13 100644 --- a/packages/inflection/revert/schemas/inflection/procedures/lower.sql +++ b/packages/inflection/revert/schemas/inflection/procedures/lower.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION inflection.lower; +DROP FUNCTION inflection.lower(text); COMMIT; diff --git a/packages/inflection/revert/schemas/inflection/procedures/no_consecutive_caps.sql b/packages/inflection/revert/schemas/inflection/procedures/no_consecutive_caps.sql index 4afef6157..93f3e48fe 100644 --- a/packages/inflection/revert/schemas/inflection/procedures/no_consecutive_caps.sql +++ b/packages/inflection/revert/schemas/inflection/procedures/no_consecutive_caps.sql @@ -2,6 +2,8 @@ BEGIN; -DROP FUNCTION inflection.no_consecutive_caps; +DROP FUNCTION inflection.no_consecutive_caps(text); +DROP FUNCTION inflection.no_consecutive_caps_till_lower(text); +DROP FUNCTION inflection.no_consecutive_caps_till_end(text); COMMIT; diff --git a/packages/inflection/revert/schemas/inflection/procedures/no_single_underscores.sql b/packages/inflection/revert/schemas/inflection/procedures/no_single_underscores.sql index 4eaf90b0a..46bd79e68 100644 --- a/packages/inflection/revert/schemas/inflection/procedures/no_single_underscores.sql +++ b/packages/inflection/revert/schemas/inflection/procedures/no_single_underscores.sql @@ -2,6 +2,9 @@ BEGIN; -DROP FUNCTION inflection.no_single_underscores; +DROP FUNCTION inflection.no_single_underscores(text); +DROP FUNCTION inflection.no_single_underscores_in_middle(text); +DROP FUNCTION inflection.no_single_underscores_at_end(text); +DROP FUNCTION inflection.no_single_underscores_in_beginning(text); COMMIT; diff --git a/packages/inflection/revert/schemas/inflection/procedures/pascal.sql b/packages/inflection/revert/schemas/inflection/procedures/pascal.sql index c0d2b843e..fa114667f 100644 --- a/packages/inflection/revert/schemas/inflection/procedures/pascal.sql +++ b/packages/inflection/revert/schemas/inflection/procedures/pascal.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION inflection.pascal; +DROP FUNCTION inflection.pascal(text); COMMIT; diff --git a/packages/inflection/revert/schemas/inflection/procedures/pg_slugify.sql b/packages/inflection/revert/schemas/inflection/procedures/pg_slugify.sql index 61c77dd42..62966f1be 100644 --- a/packages/inflection/revert/schemas/inflection/procedures/pg_slugify.sql +++ b/packages/inflection/revert/schemas/inflection/procedures/pg_slugify.sql @@ -3,6 +3,6 @@ BEGIN; DROP FUNCTION inflection.pg_slugify(text); -DROP FUNCTION inflection.pg_slugify(text, boolean); +DROP FUNCTION inflection.pg_slugify(text, bool); COMMIT; diff --git a/packages/inflection/revert/schemas/inflection/procedures/plural.sql b/packages/inflection/revert/schemas/inflection/procedures/plural.sql index de84f828b..425a5a878 100644 --- a/packages/inflection/revert/schemas/inflection/procedures/plural.sql +++ b/packages/inflection/revert/schemas/inflection/procedures/plural.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION inflection.plural; +DROP FUNCTION inflection.plural(text); COMMIT; diff --git a/packages/inflection/revert/schemas/inflection/procedures/should_skip_uncountable.sql b/packages/inflection/revert/schemas/inflection/procedures/should_skip_uncountable.sql index 62ce338f3..0918213b0 100644 --- a/packages/inflection/revert/schemas/inflection/procedures/should_skip_uncountable.sql +++ b/packages/inflection/revert/schemas/inflection/procedures/should_skip_uncountable.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION inflection.should_skip_uncountable; +DROP FUNCTION inflection.should_skip_uncountable(text); COMMIT; diff --git a/packages/inflection/revert/schemas/inflection/procedures/singular.sql b/packages/inflection/revert/schemas/inflection/procedures/singular.sql index afa571cec..80afa3a0c 100644 --- a/packages/inflection/revert/schemas/inflection/procedures/singular.sql +++ b/packages/inflection/revert/schemas/inflection/procedures/singular.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION inflection.singular; +DROP FUNCTION inflection.singular(text); COMMIT; diff --git a/packages/inflection/revert/schemas/inflection/procedures/slugify.sql b/packages/inflection/revert/schemas/inflection/procedures/slugify.sql index d0e695667..27a07c9db 100644 --- a/packages/inflection/revert/schemas/inflection/procedures/slugify.sql +++ b/packages/inflection/revert/schemas/inflection/procedures/slugify.sql @@ -3,6 +3,6 @@ BEGIN; DROP FUNCTION inflection.slugify(text); -DROP FUNCTION inflection.slugify(text, boolean); +DROP FUNCTION inflection.slugify(text, bool); COMMIT; diff --git a/packages/inflection/revert/schemas/inflection/procedures/slugify_trigger.sql b/packages/inflection/revert/schemas/inflection/procedures/slugify_trigger.sql index 8257a458e..0466d141d 100644 --- a/packages/inflection/revert/schemas/inflection/procedures/slugify_trigger.sql +++ b/packages/inflection/revert/schemas/inflection/procedures/slugify_trigger.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION inflection.slugify_trigger; +DROP FUNCTION inflection.slugify_trigger(); COMMIT; diff --git a/packages/inflection/revert/schemas/inflection/procedures/uncountable_words.sql b/packages/inflection/revert/schemas/inflection/procedures/uncountable_words.sql index 52b07ed91..b2e710b61 100644 --- a/packages/inflection/revert/schemas/inflection/procedures/uncountable_words.sql +++ b/packages/inflection/revert/schemas/inflection/procedures/uncountable_words.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION inflection.uncountable_words; +DROP FUNCTION inflection.uncountable_words(); COMMIT; diff --git a/packages/inflection/revert/schemas/inflection/procedures/upper.sql b/packages/inflection/revert/schemas/inflection/procedures/upper.sql index 2fa310a4a..72c1931d9 100644 --- a/packages/inflection/revert/schemas/inflection/procedures/upper.sql +++ b/packages/inflection/revert/schemas/inflection/procedures/upper.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION inflection.upper; +DROP FUNCTION inflection.upper(text); COMMIT; diff --git a/packages/inflection/sql/pgpm-inflection--0.36.0.bundle.tar.gz b/packages/inflection/sql/pgpm-inflection--0.36.0.bundle.tar.gz deleted file mode 100644 index 0d5617823..000000000 Binary files a/packages/inflection/sql/pgpm-inflection--0.36.0.bundle.tar.gz and /dev/null differ diff --git a/packages/inflection/sql/pgpm-inflection--0.39.0.bundle.tar.gz b/packages/inflection/sql/pgpm-inflection--0.39.0.bundle.tar.gz new file mode 100644 index 000000000..13b69c19a Binary files /dev/null and b/packages/inflection/sql/pgpm-inflection--0.39.0.bundle.tar.gz differ diff --git a/packages/inflection/sql/pgpm-inflection--0.36.0.sql b/packages/inflection/sql/pgpm-inflection--0.39.0.sql similarity index 100% rename from packages/inflection/sql/pgpm-inflection--0.36.0.sql rename to packages/inflection/sql/pgpm-inflection--0.39.0.sql diff --git a/packages/inflection/verify/schemas/inflection/procedures/camel.sql b/packages/inflection/verify/schemas/inflection/procedures/camel.sql index 08b938e72..8618f4e3f 100644 --- a/packages/inflection/verify/schemas/inflection/procedures/camel.sql +++ b/packages/inflection/verify/schemas/inflection/procedures/camel.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('inflection.camel'); +SELECT assert_function('inflection.camel(text)'::regprocedure); ROLLBACK; diff --git a/packages/inflection/verify/schemas/inflection/procedures/dashed.sql b/packages/inflection/verify/schemas/inflection/procedures/dashed.sql index 97ab5b3f3..0e1fe86a8 100644 --- a/packages/inflection/verify/schemas/inflection/procedures/dashed.sql +++ b/packages/inflection/verify/schemas/inflection/procedures/dashed.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('inflection.dashed'); +SELECT assert_function('inflection.dashed(text)'::regprocedure); ROLLBACK; diff --git a/packages/inflection/verify/schemas/inflection/procedures/dns_1123.sql b/packages/inflection/verify/schemas/inflection/procedures/dns_1123.sql index 6a0007afd..89a927e9c 100644 --- a/packages/inflection/verify/schemas/inflection/procedures/dns_1123.sql +++ b/packages/inflection/verify/schemas/inflection/procedures/dns_1123.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('inflection.dns_1123'); +SELECT assert_function('inflection.dns_1123(text)'::regprocedure); ROLLBACK; diff --git a/packages/inflection/verify/schemas/inflection/procedures/lower.sql b/packages/inflection/verify/schemas/inflection/procedures/lower.sql index 41e379df2..a8d080fdc 100644 --- a/packages/inflection/verify/schemas/inflection/procedures/lower.sql +++ b/packages/inflection/verify/schemas/inflection/procedures/lower.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('inflection.lower'); +SELECT assert_function('inflection.lower(text)'::regprocedure); ROLLBACK; diff --git a/packages/inflection/verify/schemas/inflection/procedures/no_consecutive_caps.sql b/packages/inflection/verify/schemas/inflection/procedures/no_consecutive_caps.sql index 725e139fe..11d721fb3 100644 --- a/packages/inflection/verify/schemas/inflection/procedures/no_consecutive_caps.sql +++ b/packages/inflection/verify/schemas/inflection/procedures/no_consecutive_caps.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('inflection.no_consecutive_caps'); +SELECT assert_function('inflection.no_consecutive_caps(text)'::regprocedure); ROLLBACK; diff --git a/packages/inflection/verify/schemas/inflection/procedures/no_single_underscores.sql b/packages/inflection/verify/schemas/inflection/procedures/no_single_underscores.sql index e09f21c61..09112683e 100644 --- a/packages/inflection/verify/schemas/inflection/procedures/no_single_underscores.sql +++ b/packages/inflection/verify/schemas/inflection/procedures/no_single_underscores.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('inflection.no_single_underscores'); +SELECT assert_function('inflection.no_single_underscores(text)'::regprocedure); ROLLBACK; diff --git a/packages/inflection/verify/schemas/inflection/procedures/pascal.sql b/packages/inflection/verify/schemas/inflection/procedures/pascal.sql index 06f693b35..b0c15737c 100644 --- a/packages/inflection/verify/schemas/inflection/procedures/pascal.sql +++ b/packages/inflection/verify/schemas/inflection/procedures/pascal.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('inflection.pascal'); +SELECT assert_function('inflection.pascal(text)'::regprocedure); ROLLBACK; diff --git a/packages/inflection/verify/schemas/inflection/procedures/pg_slugify.sql b/packages/inflection/verify/schemas/inflection/procedures/pg_slugify.sql index 44194412c..e7b068ebd 100644 --- a/packages/inflection/verify/schemas/inflection/procedures/pg_slugify.sql +++ b/packages/inflection/verify/schemas/inflection/procedures/pg_slugify.sql @@ -2,6 +2,7 @@ BEGIN; -SELECT verify_function ('inflection.pg_slugify'); +SELECT assert_function('inflection.pg_slugify(text, bool)'::regprocedure); +SELECT assert_function('inflection.pg_slugify(text)'::regprocedure); ROLLBACK; diff --git a/packages/inflection/verify/schemas/inflection/procedures/plural.sql b/packages/inflection/verify/schemas/inflection/procedures/plural.sql index a0aee55da..fe7028c43 100644 --- a/packages/inflection/verify/schemas/inflection/procedures/plural.sql +++ b/packages/inflection/verify/schemas/inflection/procedures/plural.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('inflection.plural'); +SELECT assert_function('inflection.plural(text)'::regprocedure); ROLLBACK; diff --git a/packages/inflection/verify/schemas/inflection/procedures/should_skip_uncountable.sql b/packages/inflection/verify/schemas/inflection/procedures/should_skip_uncountable.sql index 4c7d135ec..ad57df8c4 100644 --- a/packages/inflection/verify/schemas/inflection/procedures/should_skip_uncountable.sql +++ b/packages/inflection/verify/schemas/inflection/procedures/should_skip_uncountable.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('inflection.should_skip_uncountable'); +SELECT assert_function('inflection.should_skip_uncountable(text)'::regprocedure); ROLLBACK; diff --git a/packages/inflection/verify/schemas/inflection/procedures/singular.sql b/packages/inflection/verify/schemas/inflection/procedures/singular.sql index a39805e31..83e7e0de0 100644 --- a/packages/inflection/verify/schemas/inflection/procedures/singular.sql +++ b/packages/inflection/verify/schemas/inflection/procedures/singular.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('inflection.singular'); +SELECT assert_function('inflection.singular(text)'::regprocedure); ROLLBACK; diff --git a/packages/inflection/verify/schemas/inflection/procedures/slugify.sql b/packages/inflection/verify/schemas/inflection/procedures/slugify.sql index c824b2c4e..a23434bc7 100644 --- a/packages/inflection/verify/schemas/inflection/procedures/slugify.sql +++ b/packages/inflection/verify/schemas/inflection/procedures/slugify.sql @@ -2,6 +2,7 @@ BEGIN; -SELECT verify_function ('inflection.slugify'); +SELECT assert_function('inflection.slugify(text, bool)'::regprocedure); +SELECT assert_function('inflection.slugify(text)'::regprocedure); ROLLBACK; diff --git a/packages/inflection/verify/schemas/inflection/procedures/slugify_trigger.sql b/packages/inflection/verify/schemas/inflection/procedures/slugify_trigger.sql index bbaba2bb5..1dc1ae1dd 100644 --- a/packages/inflection/verify/schemas/inflection/procedures/slugify_trigger.sql +++ b/packages/inflection/verify/schemas/inflection/procedures/slugify_trigger.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('inflection.slugify_trigger'); +SELECT assert_function('inflection.slugify_trigger()'::regprocedure); ROLLBACK; diff --git a/packages/inflection/verify/schemas/inflection/procedures/uncountable_words.sql b/packages/inflection/verify/schemas/inflection/procedures/uncountable_words.sql index 8cf2e9c2c..40f07dead 100644 --- a/packages/inflection/verify/schemas/inflection/procedures/uncountable_words.sql +++ b/packages/inflection/verify/schemas/inflection/procedures/uncountable_words.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('inflection.uncountable_words'); +SELECT assert_function('inflection.uncountable_words()'::regprocedure); ROLLBACK; diff --git a/packages/inflection/verify/schemas/inflection/procedures/underscore.sql b/packages/inflection/verify/schemas/inflection/procedures/underscore.sql index 3b4f95976..eecb51601 100644 --- a/packages/inflection/verify/schemas/inflection/procedures/underscore.sql +++ b/packages/inflection/verify/schemas/inflection/procedures/underscore.sql @@ -2,7 +2,9 @@ BEGIN; -SELECT verify_function ('inflection.underscore(text)'); -SELECT verify_function ('inflection.underscore(text[])'); +SELECT assert_function('inflection.underscore(text)'::regprocedure); +SELECT assert_function('inflection.underscore(text[])'::regprocedure); +SELECT assert_function('inflection.underscore(text)'::regprocedure); +SELECT assert_function('inflection.underscore(text[])'::regprocedure); ROLLBACK; diff --git a/packages/inflection/verify/schemas/inflection/procedures/upper.sql b/packages/inflection/verify/schemas/inflection/procedures/upper.sql index 6ea15974a..53b368517 100644 --- a/packages/inflection/verify/schemas/inflection/procedures/upper.sql +++ b/packages/inflection/verify/schemas/inflection/procedures/upper.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('inflection.upper'); +SELECT assert_function('inflection.upper(text)'::regprocedure); ROLLBACK; diff --git a/packages/inflection/verify/schemas/inflection/schema.sql b/packages/inflection/verify/schemas/inflection/schema.sql index 45ffe8f59..34eddf0f7 100644 --- a/packages/inflection/verify/schemas/inflection/schema.sql +++ b/packages/inflection/verify/schemas/inflection/schema.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_schema ('inflection'); +SELECT assert_schema('inflection'::regnamespace); ROLLBACK; diff --git a/packages/inflection/verify/schemas/inflection/tables/inflection_rules/indexes/inflection_rules_type_idx.sql b/packages/inflection/verify/schemas/inflection/tables/inflection_rules/indexes/inflection_rules_type_idx.sql index 16b0a8cf8..e23a720de 100644 --- a/packages/inflection/verify/schemas/inflection/tables/inflection_rules/indexes/inflection_rules_type_idx.sql +++ b/packages/inflection/verify/schemas/inflection/tables/inflection_rules/indexes/inflection_rules_type_idx.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_index ('inflection.inflection_rules', 'inflection_rules_type_idx'); +SELECT assert_index('inflection.inflection_rules_type_idx'::regclass, 'inflection.inflection_rules'::regclass); ROLLBACK; diff --git a/packages/inflection/verify/schemas/inflection/tables/inflection_rules/table.sql b/packages/inflection/verify/schemas/inflection/tables/inflection_rules/table.sql index 2c45bf6fd..7f0e979b8 100644 --- a/packages/inflection/verify/schemas/inflection/tables/inflection_rules/table.sql +++ b/packages/inflection/verify/schemas/inflection/tables/inflection_rules/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('inflection.inflection_rules'); +SELECT assert_table('inflection.inflection_rules'::regclass); ROLLBACK; diff --git a/packages/jobs/Makefile b/packages/jobs/Makefile index 607e276ca..ed66fcb90 100644 --- a/packages/jobs/Makefile +++ b/packages/jobs/Makefile @@ -1,5 +1,5 @@ EXTENSION = pgpm-jobs -DATA = sql/pgpm-jobs--0.36.0.sql +DATA = sql/pgpm-jobs--0.39.0.sql PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) diff --git a/packages/jobs/package.json b/packages/jobs/package.json index 3476964dd..fadef9fd4 100644 --- a/packages/jobs/package.json +++ b/packages/jobs/package.json @@ -1,6 +1,6 @@ { "name": "@pgpm/jobs", - "version": "0.36.0", + "version": "0.39.0", "description": "Core job system for background task processing in PostgreSQL", "author": "Dan Lynch ", "contributors": [ diff --git a/packages/jobs/pgpm-jobs.control b/packages/jobs/pgpm-jobs.control index 2e667a8d7..f75cda4e8 100644 --- a/packages/jobs/pgpm-jobs.control +++ b/packages/jobs/pgpm-jobs.control @@ -1,6 +1,6 @@ # pgpm-jobs extension comment = 'pgpm-jobs extension' -default_version = '0.36.0' +default_version = '0.39.0' module_pathname = '$libdir/pgpm-jobs' requires = 'plpgsql,pgcrypto,pgpm-verify' relocatable = false diff --git a/packages/jobs/pgpm.plan b/packages/jobs/pgpm.plan index 03f017f30..b1219921a 100644 --- a/packages/jobs/pgpm.plan +++ b/packages/jobs/pgpm.plan @@ -1,7 +1,7 @@ %syntax-version=1.0.0 %project=pgpm-jobs %uri=pgpm-jobs -schemas/app_jobs/schema [pgpm-verify:@0.1.0] 2025-08-26T23:57:44Z pgpm # add schemas/app_jobs/schema +schemas/app_jobs/schema [pgpm-verify:procedures/assert_function] 2025-08-26T23:57:44Z pgpm # add schemas/app_jobs/schema schemas/app_jobs/triggers/tg_update_timestamps [schemas/app_jobs/schema] 2025-08-26T23:57:44Z pgpm # add schemas/app_jobs/triggers/tg_update_timestamps schemas/app_jobs/triggers/tg_add_job_with_row_id [schemas/app_jobs/schema] 2025-08-26T23:57:44Z pgpm # add schemas/app_jobs/triggers/tg_add_job_with_row_id schemas/app_jobs/triggers/tg_add_job_with_row [schemas/app_jobs/schema] 2025-08-26T23:57:44Z pgpm # add schemas/app_jobs/triggers/tg_add_job_with_row diff --git a/packages/jobs/revert/schemas/app_jobs/helpers/json_build_object_apply.sql b/packages/jobs/revert/schemas/app_jobs/helpers/json_build_object_apply.sql index b1778898c..9771f00af 100644 --- a/packages/jobs/revert/schemas/app_jobs/helpers/json_build_object_apply.sql +++ b/packages/jobs/revert/schemas/app_jobs/helpers/json_build_object_apply.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION app_jobs.json_build_object_apply; +DROP FUNCTION app_jobs.json_build_object_apply(text[]); COMMIT; diff --git a/packages/jobs/revert/schemas/app_jobs/procedures/add_job.sql b/packages/jobs/revert/schemas/app_jobs/procedures/add_job.sql index 44a65ae8d..7409e7978 100644 --- a/packages/jobs/revert/schemas/app_jobs/procedures/add_job.sql +++ b/packages/jobs/revert/schemas/app_jobs/procedures/add_job.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION app_jobs.add_job; +DROP FUNCTION app_jobs.add_job(text, json, text, text, timestamptz, int4, int4); COMMIT; diff --git a/packages/jobs/revert/schemas/app_jobs/procedures/add_scheduled_job.sql b/packages/jobs/revert/schemas/app_jobs/procedures/add_scheduled_job.sql index 882a98f9d..4631949ca 100644 --- a/packages/jobs/revert/schemas/app_jobs/procedures/add_scheduled_job.sql +++ b/packages/jobs/revert/schemas/app_jobs/procedures/add_scheduled_job.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION app_jobs.add_scheduled_job; +DROP FUNCTION app_jobs.add_scheduled_job(text, json, json, text, text, int4, int4); COMMIT; diff --git a/packages/jobs/revert/schemas/app_jobs/procedures/complete_job.sql b/packages/jobs/revert/schemas/app_jobs/procedures/complete_job.sql index 7c0ea9dfa..c45460937 100644 --- a/packages/jobs/revert/schemas/app_jobs/procedures/complete_job.sql +++ b/packages/jobs/revert/schemas/app_jobs/procedures/complete_job.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION app_jobs.complete_job; +DROP FUNCTION app_jobs.complete_job(text, int8); COMMIT; diff --git a/packages/jobs/revert/schemas/app_jobs/procedures/complete_jobs.sql b/packages/jobs/revert/schemas/app_jobs/procedures/complete_jobs.sql index 3db9150e3..49ab22dbc 100644 --- a/packages/jobs/revert/schemas/app_jobs/procedures/complete_jobs.sql +++ b/packages/jobs/revert/schemas/app_jobs/procedures/complete_jobs.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION app_jobs.complete_jobs; +DROP FUNCTION app_jobs.complete_jobs(int8[]); COMMIT; diff --git a/packages/jobs/revert/schemas/app_jobs/procedures/do_notify.sql b/packages/jobs/revert/schemas/app_jobs/procedures/do_notify.sql index 58a8138a1..54cb06fd0 100644 --- a/packages/jobs/revert/schemas/app_jobs/procedures/do_notify.sql +++ b/packages/jobs/revert/schemas/app_jobs/procedures/do_notify.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION app_jobs.do_notify; +DROP FUNCTION app_jobs.do_notify(); COMMIT; diff --git a/packages/jobs/revert/schemas/app_jobs/procedures/fail_job.sql b/packages/jobs/revert/schemas/app_jobs/procedures/fail_job.sql index ed96e401e..b29411dcb 100644 --- a/packages/jobs/revert/schemas/app_jobs/procedures/fail_job.sql +++ b/packages/jobs/revert/schemas/app_jobs/procedures/fail_job.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION app_jobs.fail_job; +DROP FUNCTION app_jobs.fail_job(text, int8, text); COMMIT; diff --git a/packages/jobs/revert/schemas/app_jobs/procedures/get_job.sql b/packages/jobs/revert/schemas/app_jobs/procedures/get_job.sql index 469f6b4da..0251a8a04 100644 --- a/packages/jobs/revert/schemas/app_jobs/procedures/get_job.sql +++ b/packages/jobs/revert/schemas/app_jobs/procedures/get_job.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION app_jobs.get_job; +DROP FUNCTION app_jobs.get_job(text, text[], interval); COMMIT; diff --git a/packages/jobs/revert/schemas/app_jobs/procedures/get_scheduled_job.sql b/packages/jobs/revert/schemas/app_jobs/procedures/get_scheduled_job.sql index f41f8fdb4..b51c7545d 100644 --- a/packages/jobs/revert/schemas/app_jobs/procedures/get_scheduled_job.sql +++ b/packages/jobs/revert/schemas/app_jobs/procedures/get_scheduled_job.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION app_jobs.get_scheduled_job; +DROP FUNCTION app_jobs.get_scheduled_job(text, text[]); COMMIT; diff --git a/packages/jobs/revert/schemas/app_jobs/procedures/permanently_fail_jobs.sql b/packages/jobs/revert/schemas/app_jobs/procedures/permanently_fail_jobs.sql index f0299ea82..c906e91c5 100644 --- a/packages/jobs/revert/schemas/app_jobs/procedures/permanently_fail_jobs.sql +++ b/packages/jobs/revert/schemas/app_jobs/procedures/permanently_fail_jobs.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION app_jobs.permanently_fail_jobs; +DROP FUNCTION app_jobs.permanently_fail_jobs(int8[], text); COMMIT; diff --git a/packages/jobs/revert/schemas/app_jobs/procedures/release_jobs.sql b/packages/jobs/revert/schemas/app_jobs/procedures/release_jobs.sql index 8ece69ef1..6b6ce713e 100644 --- a/packages/jobs/revert/schemas/app_jobs/procedures/release_jobs.sql +++ b/packages/jobs/revert/schemas/app_jobs/procedures/release_jobs.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION app_jobs.release_jobs; +DROP FUNCTION app_jobs.release_jobs(text); COMMIT; diff --git a/packages/jobs/revert/schemas/app_jobs/procedures/release_scheduled_jobs.sql b/packages/jobs/revert/schemas/app_jobs/procedures/release_scheduled_jobs.sql index a16e6e9a4..33005e6e3 100644 --- a/packages/jobs/revert/schemas/app_jobs/procedures/release_scheduled_jobs.sql +++ b/packages/jobs/revert/schemas/app_jobs/procedures/release_scheduled_jobs.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION app_jobs.release_scheduled_jobs; +DROP FUNCTION app_jobs.release_scheduled_jobs(text, int8[]); COMMIT; diff --git a/packages/jobs/revert/schemas/app_jobs/procedures/reschedule_jobs.sql b/packages/jobs/revert/schemas/app_jobs/procedures/reschedule_jobs.sql index 34a441716..f62607d26 100644 --- a/packages/jobs/revert/schemas/app_jobs/procedures/reschedule_jobs.sql +++ b/packages/jobs/revert/schemas/app_jobs/procedures/reschedule_jobs.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION app_jobs.reschedule_jobs; +DROP FUNCTION app_jobs.reschedule_jobs(int8[], timestamptz, int4, int4, int4); COMMIT; diff --git a/packages/jobs/revert/schemas/app_jobs/procedures/run_scheduled_job.sql b/packages/jobs/revert/schemas/app_jobs/procedures/run_scheduled_job.sql index 77886fc04..f89036df0 100644 --- a/packages/jobs/revert/schemas/app_jobs/procedures/run_scheduled_job.sql +++ b/packages/jobs/revert/schemas/app_jobs/procedures/run_scheduled_job.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION app_jobs.run_scheduled_job; +DROP FUNCTION app_jobs.run_scheduled_job(int8, interval); COMMIT; diff --git a/packages/jobs/revert/schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count.sql b/packages/jobs/revert/schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count.sql index bf4f88c6f..3181fcce4 100644 --- a/packages/jobs/revert/schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count.sql +++ b/packages/jobs/revert/schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count.sql @@ -1,7 +1,9 @@ -- Revert schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count from pg + BEGIN; -DROP TRIGGER decrease_job_queue_count_on_delete ON app_jobs.jobs; + DROP TRIGGER decrease_job_queue_count_on_update ON app_jobs.jobs; -DROP FUNCTION app_jobs.tg_decrease_job_queue_count; -COMMIT; +DROP TRIGGER decrease_job_queue_count_on_delete ON app_jobs.jobs; +DROP FUNCTION app_jobs.tg_decrease_job_queue_count(); +COMMIT; diff --git a/packages/jobs/revert/schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count.sql b/packages/jobs/revert/schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count.sql index 5098a6517..4a4094130 100644 --- a/packages/jobs/revert/schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count.sql +++ b/packages/jobs/revert/schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count.sql @@ -1,7 +1,9 @@ -- Revert schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count from pg + BEGIN; -DROP TRIGGER _500_increase_job_queue_count_on_insert ON app_jobs.jobs; + DROP TRIGGER _500_increase_job_queue_count_on_update ON app_jobs.jobs; -DROP FUNCTION app_jobs.tg_increase_job_queue_count; -COMMIT; +DROP TRIGGER _500_increase_job_queue_count_on_insert ON app_jobs.jobs; +DROP FUNCTION app_jobs.tg_increase_job_queue_count(); +COMMIT; diff --git a/packages/jobs/revert/schemas/app_jobs/triggers/tg_add_job_with_fields.sql b/packages/jobs/revert/schemas/app_jobs/triggers/tg_add_job_with_fields.sql index 5384edfc3..5ac40f23e 100644 --- a/packages/jobs/revert/schemas/app_jobs/triggers/tg_add_job_with_fields.sql +++ b/packages/jobs/revert/schemas/app_jobs/triggers/tg_add_job_with_fields.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION app_jobs.trigger_job_with_fields; +DROP FUNCTION app_jobs.trigger_job_with_fields(); COMMIT; diff --git a/packages/jobs/revert/schemas/app_jobs/triggers/tg_add_job_with_row.sql b/packages/jobs/revert/schemas/app_jobs/triggers/tg_add_job_with_row.sql index 9d6b68a2c..ad603bfbe 100644 --- a/packages/jobs/revert/schemas/app_jobs/triggers/tg_add_job_with_row.sql +++ b/packages/jobs/revert/schemas/app_jobs/triggers/tg_add_job_with_row.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION app_jobs.tg_add_job_with_row; +DROP FUNCTION app_jobs.tg_add_job_with_row(); COMMIT; diff --git a/packages/jobs/revert/schemas/app_jobs/triggers/tg_add_job_with_row_id.sql b/packages/jobs/revert/schemas/app_jobs/triggers/tg_add_job_with_row_id.sql index 1f0fb04be..9e61e9f15 100644 --- a/packages/jobs/revert/schemas/app_jobs/triggers/tg_add_job_with_row_id.sql +++ b/packages/jobs/revert/schemas/app_jobs/triggers/tg_add_job_with_row_id.sql @@ -1,5 +1,5 @@ -- Revert schemas/app_jobs/triggers/tg_add_job_with_row_id from pg BEGIN; -DROP FUNCTION app_jobs.tg_add_job_with_row_id; +DROP FUNCTION app_jobs.tg_add_job_with_row_id(); COMMIT; diff --git a/packages/jobs/revert/schemas/app_jobs/triggers/tg_update_timestamps.sql b/packages/jobs/revert/schemas/app_jobs/triggers/tg_update_timestamps.sql index 37378b148..e08b9a570 100644 --- a/packages/jobs/revert/schemas/app_jobs/triggers/tg_update_timestamps.sql +++ b/packages/jobs/revert/schemas/app_jobs/triggers/tg_update_timestamps.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION app_jobs.tg_update_timestamps; +DROP FUNCTION app_jobs.tg_update_timestamps(); COMMIT; diff --git a/packages/jobs/sql/pgpm-jobs--0.36.0.bundle.tar.gz b/packages/jobs/sql/pgpm-jobs--0.36.0.bundle.tar.gz deleted file mode 100644 index fa129e35c..000000000 Binary files a/packages/jobs/sql/pgpm-jobs--0.36.0.bundle.tar.gz and /dev/null differ diff --git a/packages/jobs/sql/pgpm-jobs--0.39.0.bundle.tar.gz b/packages/jobs/sql/pgpm-jobs--0.39.0.bundle.tar.gz new file mode 100644 index 000000000..73db78bcd Binary files /dev/null and b/packages/jobs/sql/pgpm-jobs--0.39.0.bundle.tar.gz differ diff --git a/packages/jobs/sql/pgpm-jobs--0.36.0.sql b/packages/jobs/sql/pgpm-jobs--0.39.0.sql similarity index 100% rename from packages/jobs/sql/pgpm-jobs--0.36.0.sql rename to packages/jobs/sql/pgpm-jobs--0.39.0.sql diff --git a/packages/jobs/verify/schemas/app_jobs/helpers/json_build_object_apply.sql b/packages/jobs/verify/schemas/app_jobs/helpers/json_build_object_apply.sql index e05072cfb..d1b17fbb3 100644 --- a/packages/jobs/verify/schemas/app_jobs/helpers/json_build_object_apply.sql +++ b/packages/jobs/verify/schemas/app_jobs/helpers/json_build_object_apply.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('app_jobs.json_build_object_apply'); +SELECT assert_function('app_jobs.json_build_object_apply(text[])'::regprocedure); ROLLBACK; diff --git a/packages/jobs/verify/schemas/app_jobs/procedures/add_job.sql b/packages/jobs/verify/schemas/app_jobs/procedures/add_job.sql index c841e7d04..5ebc4362d 100644 --- a/packages/jobs/verify/schemas/app_jobs/procedures/add_job.sql +++ b/packages/jobs/verify/schemas/app_jobs/procedures/add_job.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('app_jobs.add_job'); +SELECT assert_function('app_jobs.add_job(text, json, text, text, timestamptz, int4, int4)'::regprocedure); ROLLBACK; diff --git a/packages/jobs/verify/schemas/app_jobs/procedures/add_scheduled_job.sql b/packages/jobs/verify/schemas/app_jobs/procedures/add_scheduled_job.sql index a2f7d4815..736566070 100644 --- a/packages/jobs/verify/schemas/app_jobs/procedures/add_scheduled_job.sql +++ b/packages/jobs/verify/schemas/app_jobs/procedures/add_scheduled_job.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('app_jobs.add_scheduled_job'); +SELECT assert_function('app_jobs.add_scheduled_job(text, json, json, text, text, int4, int4)'::regprocedure); ROLLBACK; diff --git a/packages/jobs/verify/schemas/app_jobs/procedures/complete_job.sql b/packages/jobs/verify/schemas/app_jobs/procedures/complete_job.sql index 4bd179aee..257e4f0f2 100644 --- a/packages/jobs/verify/schemas/app_jobs/procedures/complete_job.sql +++ b/packages/jobs/verify/schemas/app_jobs/procedures/complete_job.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('app_jobs.complete_job'); +SELECT assert_function('app_jobs.complete_job(text, int8)'::regprocedure); ROLLBACK; diff --git a/packages/jobs/verify/schemas/app_jobs/procedures/complete_jobs.sql b/packages/jobs/verify/schemas/app_jobs/procedures/complete_jobs.sql index aa9a5a457..53cc86bb8 100644 --- a/packages/jobs/verify/schemas/app_jobs/procedures/complete_jobs.sql +++ b/packages/jobs/verify/schemas/app_jobs/procedures/complete_jobs.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('app_jobs.complete_jobs'); +SELECT assert_function('app_jobs.complete_jobs(int8[])'::regprocedure); ROLLBACK; diff --git a/packages/jobs/verify/schemas/app_jobs/procedures/do_notify.sql b/packages/jobs/verify/schemas/app_jobs/procedures/do_notify.sql index df64a9f48..6dd5a9b7f 100644 --- a/packages/jobs/verify/schemas/app_jobs/procedures/do_notify.sql +++ b/packages/jobs/verify/schemas/app_jobs/procedures/do_notify.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('app_jobs.do_notify'); +SELECT assert_function('app_jobs.do_notify()'::regprocedure); ROLLBACK; diff --git a/packages/jobs/verify/schemas/app_jobs/procedures/fail_job.sql b/packages/jobs/verify/schemas/app_jobs/procedures/fail_job.sql index b9c65b489..d534a7336 100644 --- a/packages/jobs/verify/schemas/app_jobs/procedures/fail_job.sql +++ b/packages/jobs/verify/schemas/app_jobs/procedures/fail_job.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('app_jobs.fail_job'); +SELECT assert_function('app_jobs.fail_job(text, int8, text)'::regprocedure); ROLLBACK; diff --git a/packages/jobs/verify/schemas/app_jobs/procedures/get_job.sql b/packages/jobs/verify/schemas/app_jobs/procedures/get_job.sql index 86170be11..5d6f06a7a 100644 --- a/packages/jobs/verify/schemas/app_jobs/procedures/get_job.sql +++ b/packages/jobs/verify/schemas/app_jobs/procedures/get_job.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('app_jobs.get_job'); +SELECT assert_function('app_jobs.get_job(text, text[], interval)'::regprocedure); ROLLBACK; diff --git a/packages/jobs/verify/schemas/app_jobs/procedures/get_scheduled_job.sql b/packages/jobs/verify/schemas/app_jobs/procedures/get_scheduled_job.sql index bb7e58d7f..6a80e0f88 100644 --- a/packages/jobs/verify/schemas/app_jobs/procedures/get_scheduled_job.sql +++ b/packages/jobs/verify/schemas/app_jobs/procedures/get_scheduled_job.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('app_jobs.get_scheduled_job'); +SELECT assert_function('app_jobs.get_scheduled_job(text, text[])'::regprocedure); ROLLBACK; diff --git a/packages/jobs/verify/schemas/app_jobs/procedures/grants/grant_execute_add_job_to_authenticated.sql b/packages/jobs/verify/schemas/app_jobs/procedures/grants/grant_execute_add_job_to_authenticated.sql index dfd910bcb..cf27f9a09 100644 --- a/packages/jobs/verify/schemas/app_jobs/procedures/grants/grant_execute_add_job_to_authenticated.sql +++ b/packages/jobs/verify/schemas/app_jobs/procedures/grants/grant_execute_add_job_to_authenticated.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT has_function_privilege('authenticated', 'app_jobs.add_job(text, json, text, text, timestamptz, integer, integer)', 'EXECUTE'); +SELECT assert_function_grant('app_jobs.add_job(text, json, text, text, timestamptz, integer, integer)'::regprocedure, 'authenticated', 'EXECUTE'); ROLLBACK; diff --git a/packages/jobs/verify/schemas/app_jobs/procedures/permanently_fail_jobs.sql b/packages/jobs/verify/schemas/app_jobs/procedures/permanently_fail_jobs.sql index dfd8852f0..62596780d 100644 --- a/packages/jobs/verify/schemas/app_jobs/procedures/permanently_fail_jobs.sql +++ b/packages/jobs/verify/schemas/app_jobs/procedures/permanently_fail_jobs.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('app_jobs.permanently_fail_jobs'); +SELECT assert_function('app_jobs.permanently_fail_jobs(int8[], text)'::regprocedure); ROLLBACK; diff --git a/packages/jobs/verify/schemas/app_jobs/procedures/release_jobs.sql b/packages/jobs/verify/schemas/app_jobs/procedures/release_jobs.sql index 70004e7e1..9ff6962de 100644 --- a/packages/jobs/verify/schemas/app_jobs/procedures/release_jobs.sql +++ b/packages/jobs/verify/schemas/app_jobs/procedures/release_jobs.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('app_jobs.release_jobs'); +SELECT assert_function('app_jobs.release_jobs(text)'::regprocedure); ROLLBACK; diff --git a/packages/jobs/verify/schemas/app_jobs/procedures/release_scheduled_jobs.sql b/packages/jobs/verify/schemas/app_jobs/procedures/release_scheduled_jobs.sql index 5b9b5929a..82d6b9a64 100644 --- a/packages/jobs/verify/schemas/app_jobs/procedures/release_scheduled_jobs.sql +++ b/packages/jobs/verify/schemas/app_jobs/procedures/release_scheduled_jobs.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('app_jobs.release_scheduled_jobs'); +SELECT assert_function('app_jobs.release_scheduled_jobs(text, int8[])'::regprocedure); ROLLBACK; diff --git a/packages/jobs/verify/schemas/app_jobs/procedures/reschedule_jobs.sql b/packages/jobs/verify/schemas/app_jobs/procedures/reschedule_jobs.sql index 80ab587b3..104cd33bd 100644 --- a/packages/jobs/verify/schemas/app_jobs/procedures/reschedule_jobs.sql +++ b/packages/jobs/verify/schemas/app_jobs/procedures/reschedule_jobs.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('app_jobs.reschedule_jobs'); +SELECT assert_function('app_jobs.reschedule_jobs(int8[], timestamptz, int4, int4, int4)'::regprocedure); ROLLBACK; diff --git a/packages/jobs/verify/schemas/app_jobs/procedures/run_scheduled_job.sql b/packages/jobs/verify/schemas/app_jobs/procedures/run_scheduled_job.sql index 02257023b..9f7d02cc9 100644 --- a/packages/jobs/verify/schemas/app_jobs/procedures/run_scheduled_job.sql +++ b/packages/jobs/verify/schemas/app_jobs/procedures/run_scheduled_job.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('app_jobs.run_scheduled_job'); +SELECT assert_function('app_jobs.run_scheduled_job(int8, interval)'::regprocedure); ROLLBACK; diff --git a/packages/jobs/verify/schemas/app_jobs/schema.sql b/packages/jobs/verify/schemas/app_jobs/schema.sql index 5e0b19d49..cb33f345b 100644 --- a/packages/jobs/verify/schemas/app_jobs/schema.sql +++ b/packages/jobs/verify/schemas/app_jobs/schema.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_schema ('app_jobs'); +SELECT assert_schema('app_jobs'::regnamespace); ROLLBACK; diff --git a/packages/jobs/verify/schemas/app_jobs/tables/job_queues/grants/grant_select_insert_update_delete_to_administrator.sql b/packages/jobs/verify/schemas/app_jobs/tables/job_queues/grants/grant_select_insert_update_delete_to_administrator.sql index d645d8558..433920c99 100644 --- a/packages/jobs/verify/schemas/app_jobs/tables/job_queues/grants/grant_select_insert_update_delete_to_administrator.sql +++ b/packages/jobs/verify/schemas/app_jobs/tables/job_queues/grants/grant_select_insert_update_delete_to_administrator.sql @@ -2,9 +2,9 @@ BEGIN; - SELECT has_table_privilege('administrator', 'app_jobs.job_queues', 'SELECT'); - SELECT has_table_privilege('administrator', 'app_jobs.job_queues', 'INSERT'); - SELECT has_table_privilege('administrator', 'app_jobs.job_queues', 'UPDATE'); - SELECT has_table_privilege('administrator', 'app_jobs.job_queues', 'DELETE'); + SELECT assert_table_grant('app_jobs.job_queues'::regclass, 'administrator', 'SELECT'); + SELECT assert_table_grant('app_jobs.job_queues'::regclass, 'administrator', 'INSERT'); + SELECT assert_table_grant('app_jobs.job_queues'::regclass, 'administrator', 'UPDATE'); + SELECT assert_table_grant('app_jobs.job_queues'::regclass, 'administrator', 'DELETE'); ROLLBACK; diff --git a/packages/jobs/verify/schemas/app_jobs/tables/job_queues/indexes/job_queues_locked_by_idx.sql b/packages/jobs/verify/schemas/app_jobs/tables/job_queues/indexes/job_queues_locked_by_idx.sql index bb3786608..e5dc9536b 100644 --- a/packages/jobs/verify/schemas/app_jobs/tables/job_queues/indexes/job_queues_locked_by_idx.sql +++ b/packages/jobs/verify/schemas/app_jobs/tables/job_queues/indexes/job_queues_locked_by_idx.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_index ('app_jobs.job_queues', 'job_queues_locked_by_idx'); +SELECT assert_index('app_jobs.job_queues_locked_by_idx'::regclass, 'app_jobs.job_queues'::regclass); ROLLBACK; diff --git a/packages/jobs/verify/schemas/app_jobs/tables/job_queues/table.sql b/packages/jobs/verify/schemas/app_jobs/tables/job_queues/table.sql index 3a5e4b1cd..b63fc4af4 100644 --- a/packages/jobs/verify/schemas/app_jobs/tables/job_queues/table.sql +++ b/packages/jobs/verify/schemas/app_jobs/tables/job_queues/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('app_jobs.job_queues'); +SELECT assert_table('app_jobs.job_queues'::regclass); ROLLBACK; diff --git a/packages/jobs/verify/schemas/app_jobs/tables/jobs/grants/grant_select_insert_update_delete_to_administrator.sql b/packages/jobs/verify/schemas/app_jobs/tables/jobs/grants/grant_select_insert_update_delete_to_administrator.sql index 6255d7164..f2da0c696 100644 --- a/packages/jobs/verify/schemas/app_jobs/tables/jobs/grants/grant_select_insert_update_delete_to_administrator.sql +++ b/packages/jobs/verify/schemas/app_jobs/tables/jobs/grants/grant_select_insert_update_delete_to_administrator.sql @@ -2,9 +2,9 @@ BEGIN; - SELECT has_table_privilege('administrator', 'app_jobs.jobs', 'SELECT'); - SELECT has_table_privilege('administrator', 'app_jobs.jobs', 'INSERT'); - SELECT has_table_privilege('administrator', 'app_jobs.jobs', 'UPDATE'); - SELECT has_table_privilege('administrator', 'app_jobs.jobs', 'DELETE'); + SELECT assert_table_grant('app_jobs.jobs'::regclass, 'administrator', 'SELECT'); + SELECT assert_table_grant('app_jobs.jobs'::regclass, 'administrator', 'INSERT'); + SELECT assert_table_grant('app_jobs.jobs'::regclass, 'administrator', 'UPDATE'); + SELECT assert_table_grant('app_jobs.jobs'::regclass, 'administrator', 'DELETE'); ROLLBACK; diff --git a/packages/jobs/verify/schemas/app_jobs/tables/jobs/indexes/jobs_locked_by_idx.sql b/packages/jobs/verify/schemas/app_jobs/tables/jobs/indexes/jobs_locked_by_idx.sql index 3635677ae..b6d72c45c 100644 --- a/packages/jobs/verify/schemas/app_jobs/tables/jobs/indexes/jobs_locked_by_idx.sql +++ b/packages/jobs/verify/schemas/app_jobs/tables/jobs/indexes/jobs_locked_by_idx.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_index ('app_jobs.jobs', 'jobs_locked_by_idx'); +SELECT assert_index('app_jobs.jobs_locked_by_idx'::regclass, 'app_jobs.jobs'::regclass); ROLLBACK; diff --git a/packages/jobs/verify/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql b/packages/jobs/verify/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql index 2fc8b4ccd..da80d54dd 100644 --- a/packages/jobs/verify/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql +++ b/packages/jobs/verify/schemas/app_jobs/tables/jobs/indexes/priority_run_at_id_idx.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_index ('app_jobs.jobs', 'priority_run_at_id_idx'); +SELECT assert_index('app_jobs.priority_run_at_id_idx'::regclass, 'app_jobs.jobs'::regclass); ROLLBACK; diff --git a/packages/jobs/verify/schemas/app_jobs/tables/jobs/table.sql b/packages/jobs/verify/schemas/app_jobs/tables/jobs/table.sql index aaa0584da..30c45d200 100644 --- a/packages/jobs/verify/schemas/app_jobs/tables/jobs/table.sql +++ b/packages/jobs/verify/schemas/app_jobs/tables/jobs/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('app_jobs.jobs'); +SELECT assert_table('app_jobs.jobs'::regclass); ROLLBACK; diff --git a/packages/jobs/verify/schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count.sql b/packages/jobs/verify/schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count.sql index 97b717d08..bf960d874 100644 --- a/packages/jobs/verify/schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count.sql +++ b/packages/jobs/verify/schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count.sql @@ -1,10 +1,23 @@ -- Verify schemas/app_jobs/tables/jobs/triggers/decrease_job_queue_count on pg + BEGIN; -SELECT - verify_function ('app_jobs.tg_decrease_job_queue_count'); -SELECT - verify_trigger ('app_jobs.decrease_job_queue_count_on_delete'); -SELECT - verify_trigger ('app_jobs.decrease_job_queue_count_on_update'); -ROLLBACK; +SELECT assert_function('app_jobs.tg_decrease_job_queue_count()'::regprocedure, 'trigger'::regtype); + +-- AFTER (0) DELETE (8) FOR EACH ROW (1). +SELECT assert_trigger( + 'app_jobs.jobs'::regclass, + 'decrease_job_queue_count_on_delete', + 'app_jobs.tg_decrease_job_queue_count'::regproc, + 9 +); + +-- AFTER (0) UPDATE (16) FOR EACH ROW (1). +SELECT assert_trigger( + 'app_jobs.jobs'::regclass, + 'decrease_job_queue_count_on_update', + 'app_jobs.tg_decrease_job_queue_count'::regproc, + 17 +); + +ROLLBACK; diff --git a/packages/jobs/verify/schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count.sql b/packages/jobs/verify/schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count.sql index a6e89dd17..d84e959e4 100644 --- a/packages/jobs/verify/schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count.sql +++ b/packages/jobs/verify/schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count.sql @@ -1,10 +1,23 @@ -- Verify schemas/app_jobs/tables/jobs/triggers/increase_job_queue_count on pg + BEGIN; -SELECT - verify_function ('app_jobs.tg_increase_job_queue_count'); -SELECT - verify_trigger ('app_jobs._500_increase_job_queue_count_on_insert'); -SELECT - verify_trigger ('app_jobs._500_increase_job_queue_count_on_update'); -ROLLBACK; +SELECT assert_function('app_jobs.tg_increase_job_queue_count()'::regprocedure, 'trigger'::regtype); + +-- AFTER (0) INSERT (4) FOR EACH ROW (1). +SELECT assert_trigger( + 'app_jobs.jobs'::regclass, + '_500_increase_job_queue_count_on_insert', + 'app_jobs.tg_increase_job_queue_count'::regproc, + 5 +); + +-- AFTER (0) UPDATE (16) FOR EACH ROW (1). +SELECT assert_trigger( + 'app_jobs.jobs'::regclass, + '_500_increase_job_queue_count_on_update', + 'app_jobs.tg_increase_job_queue_count'::regproc, + 17 +); + +ROLLBACK; diff --git a/packages/jobs/verify/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql b/packages/jobs/verify/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql index dc0436efa..ee58ffed9 100644 --- a/packages/jobs/verify/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql +++ b/packages/jobs/verify/schemas/app_jobs/tables/jobs/triggers/notify_worker.sql @@ -1,6 +1,13 @@ -- Verify schemas/app_jobs/tables/jobs/triggers/notify_worker on pg + BEGIN; -SELECT - verify_trigger ('app_jobs._900_notify_worker'); -ROLLBACK; +-- AFTER (0) INSERT (4) FOR EACH ROW (1). +SELECT assert_trigger( + 'app_jobs.jobs'::regclass, + '_900_notify_worker', + 'app_jobs.do_notify'::regproc, + 5 +); + +ROLLBACK; diff --git a/packages/jobs/verify/schemas/app_jobs/tables/jobs/triggers/timestamps.sql b/packages/jobs/verify/schemas/app_jobs/tables/jobs/triggers/timestamps.sql index ed9466a37..fd7bac1c4 100644 --- a/packages/jobs/verify/schemas/app_jobs/tables/jobs/triggers/timestamps.sql +++ b/packages/jobs/verify/schemas/app_jobs/tables/jobs/triggers/timestamps.sql @@ -1,16 +1,25 @@ -- Verify schemas/app_jobs/tables/jobs/triggers/timestamps on pg + BEGIN; + SELECT created_at FROM app_jobs.jobs LIMIT 1; + SELECT updated_at FROM app_jobs.jobs LIMIT 1; -SELECT - verify_trigger ('app_jobs._100_update_jobs_modtime_tg'); -ROLLBACK; +-- BEFORE (2) INSERT (4) OR UPDATE (16) FOR EACH ROW (1). +SELECT assert_trigger( + 'app_jobs.jobs'::regclass, + '_100_update_jobs_modtime_tg', + 'app_jobs.tg_update_timestamps'::regproc, + 23 +); + +ROLLBACK; diff --git a/packages/jobs/verify/schemas/app_jobs/tables/scheduled_jobs/grants/grant_select_insert_update_delete_to_administrator.sql b/packages/jobs/verify/schemas/app_jobs/tables/scheduled_jobs/grants/grant_select_insert_update_delete_to_administrator.sql index c4aa4eb6c..781fee1a1 100644 --- a/packages/jobs/verify/schemas/app_jobs/tables/scheduled_jobs/grants/grant_select_insert_update_delete_to_administrator.sql +++ b/packages/jobs/verify/schemas/app_jobs/tables/scheduled_jobs/grants/grant_select_insert_update_delete_to_administrator.sql @@ -2,9 +2,9 @@ BEGIN; - SELECT has_table_privilege('administrator', 'app_jobs.scheduled_jobs', 'SELECT'); - SELECT has_table_privilege('administrator', 'app_jobs.scheduled_jobs', 'INSERT'); - SELECT has_table_privilege('administrator', 'app_jobs.scheduled_jobs', 'UPDATE'); - SELECT has_table_privilege('administrator', 'app_jobs.scheduled_jobs', 'DELETE'); + SELECT assert_table_grant('app_jobs.scheduled_jobs'::regclass, 'administrator', 'SELECT'); + SELECT assert_table_grant('app_jobs.scheduled_jobs'::regclass, 'administrator', 'INSERT'); + SELECT assert_table_grant('app_jobs.scheduled_jobs'::regclass, 'administrator', 'UPDATE'); + SELECT assert_table_grant('app_jobs.scheduled_jobs'::regclass, 'administrator', 'DELETE'); ROLLBACK; diff --git a/packages/jobs/verify/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_locked_by_idx.sql b/packages/jobs/verify/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_locked_by_idx.sql index 34ee9f117..0daf6e4ca 100644 --- a/packages/jobs/verify/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_locked_by_idx.sql +++ b/packages/jobs/verify/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_locked_by_idx.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_index ('app_jobs.scheduled_jobs', 'scheduled_jobs_locked_by_idx'); +SELECT assert_index('app_jobs.scheduled_jobs_locked_by_idx'::regclass, 'app_jobs.scheduled_jobs'::regclass); ROLLBACK; diff --git a/packages/jobs/verify/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_priority_id_idx.sql b/packages/jobs/verify/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_priority_id_idx.sql index d26a68223..3f98b247f 100644 --- a/packages/jobs/verify/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_priority_id_idx.sql +++ b/packages/jobs/verify/schemas/app_jobs/tables/scheduled_jobs/indexes/scheduled_jobs_priority_id_idx.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_index ('app_jobs.scheduled_jobs', 'scheduled_jobs_priority_id_idx'); +SELECT assert_index('app_jobs.scheduled_jobs_priority_id_idx'::regclass, 'app_jobs.scheduled_jobs'::regclass); ROLLBACK; diff --git a/packages/jobs/verify/schemas/app_jobs/tables/scheduled_jobs/table.sql b/packages/jobs/verify/schemas/app_jobs/tables/scheduled_jobs/table.sql index 065f427b1..0966ef07f 100644 --- a/packages/jobs/verify/schemas/app_jobs/tables/scheduled_jobs/table.sql +++ b/packages/jobs/verify/schemas/app_jobs/tables/scheduled_jobs/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('app_jobs.scheduled_jobs'); +SELECT assert_table('app_jobs.scheduled_jobs'::regclass); ROLLBACK; diff --git a/packages/jobs/verify/schemas/app_jobs/tables/scheduled_jobs/triggers/notify_scheduled_job.sql b/packages/jobs/verify/schemas/app_jobs/tables/scheduled_jobs/triggers/notify_scheduled_job.sql index 599c63a3a..13559df1c 100644 --- a/packages/jobs/verify/schemas/app_jobs/tables/scheduled_jobs/triggers/notify_scheduled_job.sql +++ b/packages/jobs/verify/schemas/app_jobs/tables/scheduled_jobs/triggers/notify_scheduled_job.sql @@ -3,6 +3,6 @@ BEGIN; -SELECT verify_trigger ('app_jobs._900_notify_scheduled_job'); +SELECT assert_trigger('app_jobs.scheduled_jobs'::regclass, '_900_notify_scheduled_job', 'app_jobs.do_notify'::regproc, 5); ROLLBACK; diff --git a/packages/jobs/verify/schemas/app_jobs/triggers/tg_add_job_with_fields.sql b/packages/jobs/verify/schemas/app_jobs/triggers/tg_add_job_with_fields.sql index 9b36e4f2e..aa3af3205 100644 --- a/packages/jobs/verify/schemas/app_jobs/triggers/tg_add_job_with_fields.sql +++ b/packages/jobs/verify/schemas/app_jobs/triggers/tg_add_job_with_fields.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('app_jobs.trigger_job_with_fields'); +SELECT assert_function('app_jobs.trigger_job_with_fields()'::regprocedure); ROLLBACK; diff --git a/packages/jobs/verify/schemas/app_jobs/triggers/tg_add_job_with_row.sql b/packages/jobs/verify/schemas/app_jobs/triggers/tg_add_job_with_row.sql index bdf8cc7eb..cf472d3fc 100644 --- a/packages/jobs/verify/schemas/app_jobs/triggers/tg_add_job_with_row.sql +++ b/packages/jobs/verify/schemas/app_jobs/triggers/tg_add_job_with_row.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('app_jobs.tg_add_job_with_row'); +SELECT assert_function('app_jobs.tg_add_job_with_row()'::regprocedure); ROLLBACK; diff --git a/packages/jobs/verify/schemas/app_jobs/triggers/tg_add_job_with_row_id.sql b/packages/jobs/verify/schemas/app_jobs/triggers/tg_add_job_with_row_id.sql index 72b5a7b90..f64627ea4 100644 --- a/packages/jobs/verify/schemas/app_jobs/triggers/tg_add_job_with_row_id.sql +++ b/packages/jobs/verify/schemas/app_jobs/triggers/tg_add_job_with_row_id.sql @@ -1,6 +1,7 @@ -- Verify schemas/app_jobs/triggers/tg_add_job_with_row_id on pg + BEGIN; -SELECT - verify_function ('app_jobs.tg_add_job_with_row_id'); -ROLLBACK; +SELECT assert_function('app_jobs.tg_add_job_with_row_id()'::regprocedure, 'trigger'::regtype); + +ROLLBACK; diff --git a/packages/jobs/verify/schemas/app_jobs/triggers/tg_update_timestamps.sql b/packages/jobs/verify/schemas/app_jobs/triggers/tg_update_timestamps.sql index fd53ed3e9..16fee810a 100644 --- a/packages/jobs/verify/schemas/app_jobs/triggers/tg_update_timestamps.sql +++ b/packages/jobs/verify/schemas/app_jobs/triggers/tg_update_timestamps.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('app_jobs.tg_update_timestamps'); +SELECT assert_function('app_jobs.tg_update_timestamps()'::regprocedure); ROLLBACK; diff --git a/packages/jwt-claims/Makefile b/packages/jwt-claims/Makefile index af688a59c..3e31ebbb8 100644 --- a/packages/jwt-claims/Makefile +++ b/packages/jwt-claims/Makefile @@ -1,5 +1,5 @@ EXTENSION = pgpm-jwt-claims -DATA = sql/pgpm-jwt-claims--0.36.0.sql +DATA = sql/pgpm-jwt-claims--0.39.0.sql PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) diff --git a/packages/jwt-claims/package.json b/packages/jwt-claims/package.json index 661b3f5c8..dd7319610 100644 --- a/packages/jwt-claims/package.json +++ b/packages/jwt-claims/package.json @@ -1,6 +1,6 @@ { "name": "@pgpm/jwt-claims", - "version": "0.36.0", + "version": "0.39.0", "description": "JWT claim handling and validation functions", "author": "Dan Lynch ", "contributors": [ diff --git a/packages/jwt-claims/pgpm-jwt-claims.control b/packages/jwt-claims/pgpm-jwt-claims.control index f87ffb023..50497d7e5 100644 --- a/packages/jwt-claims/pgpm-jwt-claims.control +++ b/packages/jwt-claims/pgpm-jwt-claims.control @@ -1,6 +1,6 @@ # pgpm-jwt-claims extension comment = 'pgpm-jwt-claims extension' -default_version = '0.36.0' +default_version = '0.39.0' module_pathname = '$libdir/pgpm-jwt-claims' requires = 'plpgsql,errors,pgpm-types,pgpm-verify' relocatable = false diff --git a/packages/jwt-claims/revert/schemas/ctx/procedures/ip_address.sql b/packages/jwt-claims/revert/schemas/ctx/procedures/ip_address.sql index b23230c09..b4a4ec062 100644 --- a/packages/jwt-claims/revert/schemas/ctx/procedures/ip_address.sql +++ b/packages/jwt-claims/revert/schemas/ctx/procedures/ip_address.sql @@ -2,7 +2,6 @@ BEGIN; -DROP FUNCTION ctx.ip_address; +DROP FUNCTION ctx.ip_address(); COMMIT; - diff --git a/packages/jwt-claims/revert/schemas/ctx/procedures/origin.sql b/packages/jwt-claims/revert/schemas/ctx/procedures/origin.sql index e04aae2b5..46808791c 100644 --- a/packages/jwt-claims/revert/schemas/ctx/procedures/origin.sql +++ b/packages/jwt-claims/revert/schemas/ctx/procedures/origin.sql @@ -2,7 +2,6 @@ BEGIN; -DROP FUNCTION ctx.origin; +DROP FUNCTION ctx.origin(); COMMIT; - diff --git a/packages/jwt-claims/revert/schemas/ctx/procedures/security_definer.sql b/packages/jwt-claims/revert/schemas/ctx/procedures/security_definer.sql index dbf6868db..c664262ea 100644 --- a/packages/jwt-claims/revert/schemas/ctx/procedures/security_definer.sql +++ b/packages/jwt-claims/revert/schemas/ctx/procedures/security_definer.sql @@ -2,8 +2,8 @@ BEGIN; -DROP FUNCTION ctx.security_definer; -DROP FUNCTION ctx.is_security_definer; +DROP FUNCTION ctx.security_definer(); +DROP FUNCTION ctx.is_security_definer(); COMMIT; diff --git a/packages/jwt-claims/revert/schemas/ctx/procedures/uagent.sql b/packages/jwt-claims/revert/schemas/ctx/procedures/uagent.sql index 8404c504d..811b4fdcf 100644 --- a/packages/jwt-claims/revert/schemas/ctx/procedures/uagent.sql +++ b/packages/jwt-claims/revert/schemas/ctx/procedures/uagent.sql @@ -2,7 +2,6 @@ BEGIN; -DROP FUNCTION ctx.uagent; +DROP FUNCTION ctx.uagent(); COMMIT; - diff --git a/packages/jwt-claims/revert/schemas/ctx/procedures/uid.sql b/packages/jwt-claims/revert/schemas/ctx/procedures/uid.sql index 2beaa012d..dd65077ed 100644 --- a/packages/jwt-claims/revert/schemas/ctx/procedures/uid.sql +++ b/packages/jwt-claims/revert/schemas/ctx/procedures/uid.sql @@ -2,7 +2,6 @@ BEGIN; -DROP FUNCTION ctx.uid; +DROP FUNCTION ctx.uid(); COMMIT; - diff --git a/packages/jwt-claims/revert/schemas/jwt_private/procedures/current_api_id.sql b/packages/jwt-claims/revert/schemas/jwt_private/procedures/current_api_id.sql index 1aa3218af..bb26ce901 100644 --- a/packages/jwt-claims/revert/schemas/jwt_private/procedures/current_api_id.sql +++ b/packages/jwt-claims/revert/schemas/jwt_private/procedures/current_api_id.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION jwt_private.current_api_id; +DROP FUNCTION jwt_private.current_api_id(); COMMIT; diff --git a/packages/jwt-claims/revert/schemas/jwt_private/procedures/current_database_id.sql b/packages/jwt-claims/revert/schemas/jwt_private/procedures/current_database_id.sql index 815e6da1a..b12400350 100644 --- a/packages/jwt-claims/revert/schemas/jwt_private/procedures/current_database_id.sql +++ b/packages/jwt-claims/revert/schemas/jwt_private/procedures/current_database_id.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION jwt_private.current_database_id; +DROP FUNCTION jwt_private.current_database_id(); COMMIT; diff --git a/packages/jwt-claims/revert/schemas/jwt_private/procedures/current_graph_execution_id.sql b/packages/jwt-claims/revert/schemas/jwt_private/procedures/current_graph_execution_id.sql index 0b0af2bd6..35c1fa1ac 100644 --- a/packages/jwt-claims/revert/schemas/jwt_private/procedures/current_graph_execution_id.sql +++ b/packages/jwt-claims/revert/schemas/jwt_private/procedures/current_graph_execution_id.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION jwt_private.current_graph_execution_id; +DROP FUNCTION jwt_private.current_graph_execution_id(); COMMIT; diff --git a/packages/jwt-claims/revert/schemas/jwt_private/procedures/current_session_id.sql b/packages/jwt-claims/revert/schemas/jwt_private/procedures/current_session_id.sql index fb07278e0..e0252eff6 100644 --- a/packages/jwt-claims/revert/schemas/jwt_private/procedures/current_session_id.sql +++ b/packages/jwt-claims/revert/schemas/jwt_private/procedures/current_session_id.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION jwt_private.current_session_id; +DROP FUNCTION jwt_private.current_session_id(); COMMIT; diff --git a/packages/jwt-claims/revert/schemas/jwt_private/procedures/current_token_id.sql b/packages/jwt-claims/revert/schemas/jwt_private/procedures/current_token_id.sql index f2689e67e..191e1245f 100644 --- a/packages/jwt-claims/revert/schemas/jwt_private/procedures/current_token_id.sql +++ b/packages/jwt-claims/revert/schemas/jwt_private/procedures/current_token_id.sql @@ -2,7 +2,6 @@ BEGIN; -DROP FUNCTION jwt_private.current_token_id; +DROP FUNCTION jwt_private.current_token_id(); COMMIT; - diff --git a/packages/jwt-claims/revert/schemas/jwt_public/procedures/current_ip_address.sql b/packages/jwt-claims/revert/schemas/jwt_public/procedures/current_ip_address.sql index 297da8dc4..f5d6a15a8 100644 --- a/packages/jwt-claims/revert/schemas/jwt_public/procedures/current_ip_address.sql +++ b/packages/jwt-claims/revert/schemas/jwt_public/procedures/current_ip_address.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION jwt_public.current_ip_address; +DROP FUNCTION jwt_public.current_ip_address(); COMMIT; diff --git a/packages/jwt-claims/revert/schemas/jwt_public/procedures/current_origin.sql b/packages/jwt-claims/revert/schemas/jwt_public/procedures/current_origin.sql index 98f18faa8..cd90098d3 100644 --- a/packages/jwt-claims/revert/schemas/jwt_public/procedures/current_origin.sql +++ b/packages/jwt-claims/revert/schemas/jwt_public/procedures/current_origin.sql @@ -2,7 +2,6 @@ BEGIN; -DROP FUNCTION jwt_public.current_origin; +DROP FUNCTION jwt_public.current_origin(); COMMIT; - diff --git a/packages/jwt-claims/revert/schemas/jwt_public/procedures/current_principal_id.sql b/packages/jwt-claims/revert/schemas/jwt_public/procedures/current_principal_id.sql index b1a4b6c3e..5b335a0c7 100644 --- a/packages/jwt-claims/revert/schemas/jwt_public/procedures/current_principal_id.sql +++ b/packages/jwt-claims/revert/schemas/jwt_public/procedures/current_principal_id.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION IF EXISTS jwt_public.current_principal_id(); +DROP FUNCTION jwt_public.current_principal_id(); COMMIT; diff --git a/packages/jwt-claims/revert/schemas/jwt_public/procedures/current_role_type.sql b/packages/jwt-claims/revert/schemas/jwt_public/procedures/current_role_type.sql index f9ffba4a4..68a58d193 100644 --- a/packages/jwt-claims/revert/schemas/jwt_public/procedures/current_role_type.sql +++ b/packages/jwt-claims/revert/schemas/jwt_public/procedures/current_role_type.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION IF EXISTS jwt_public.current_role_type(); +DROP FUNCTION jwt_public.current_role_type(); COMMIT; diff --git a/packages/jwt-claims/revert/schemas/jwt_public/procedures/current_user_agent.sql b/packages/jwt-claims/revert/schemas/jwt_public/procedures/current_user_agent.sql index 864e219c3..c379d7716 100644 --- a/packages/jwt-claims/revert/schemas/jwt_public/procedures/current_user_agent.sql +++ b/packages/jwt-claims/revert/schemas/jwt_public/procedures/current_user_agent.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION jwt_public.current_user_agent; +DROP FUNCTION jwt_public.current_user_agent(); COMMIT; diff --git a/packages/jwt-claims/revert/schemas/jwt_public/procedures/current_user_id.sql b/packages/jwt-claims/revert/schemas/jwt_public/procedures/current_user_id.sql index 42b48f9ae..bcae4429e 100644 --- a/packages/jwt-claims/revert/schemas/jwt_public/procedures/current_user_id.sql +++ b/packages/jwt-claims/revert/schemas/jwt_public/procedures/current_user_id.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION jwt_public.current_user_id; +DROP FUNCTION jwt_public.current_user_id(); COMMIT; diff --git a/packages/jwt-claims/sql/pgpm-jwt-claims--0.36.0.bundle.tar.gz b/packages/jwt-claims/sql/pgpm-jwt-claims--0.36.0.bundle.tar.gz deleted file mode 100644 index dfe058425..000000000 Binary files a/packages/jwt-claims/sql/pgpm-jwt-claims--0.36.0.bundle.tar.gz and /dev/null differ diff --git a/packages/jwt-claims/sql/pgpm-jwt-claims--0.39.0.bundle.tar.gz b/packages/jwt-claims/sql/pgpm-jwt-claims--0.39.0.bundle.tar.gz new file mode 100644 index 000000000..933fa3208 Binary files /dev/null and b/packages/jwt-claims/sql/pgpm-jwt-claims--0.39.0.bundle.tar.gz differ diff --git a/packages/jwt-claims/sql/pgpm-jwt-claims--0.36.0.sql b/packages/jwt-claims/sql/pgpm-jwt-claims--0.39.0.sql similarity index 100% rename from packages/jwt-claims/sql/pgpm-jwt-claims--0.36.0.sql rename to packages/jwt-claims/sql/pgpm-jwt-claims--0.39.0.sql diff --git a/packages/jwt-claims/verify/schemas/ctx/procedures/ip_address.sql b/packages/jwt-claims/verify/schemas/ctx/procedures/ip_address.sql index 191af4552..3c407eee7 100644 --- a/packages/jwt-claims/verify/schemas/ctx/procedures/ip_address.sql +++ b/packages/jwt-claims/verify/schemas/ctx/procedures/ip_address.sql @@ -2,7 +2,7 @@ BEGIN; -SELECT verify_function ('ctx.ip_address'); +SELECT assert_function('ctx.ip_address()'::regprocedure); ROLLBACK; diff --git a/packages/jwt-claims/verify/schemas/ctx/procedures/origin.sql b/packages/jwt-claims/verify/schemas/ctx/procedures/origin.sql index 4fb286ed8..55462e991 100644 --- a/packages/jwt-claims/verify/schemas/ctx/procedures/origin.sql +++ b/packages/jwt-claims/verify/schemas/ctx/procedures/origin.sql @@ -2,7 +2,7 @@ BEGIN; -SELECT verify_function ('ctx.origin'); +SELECT assert_function('ctx.origin()'::regprocedure); ROLLBACK; diff --git a/packages/jwt-claims/verify/schemas/ctx/procedures/security_definer.sql b/packages/jwt-claims/verify/schemas/ctx/procedures/security_definer.sql index d9aa4078d..15984f862 100644 --- a/packages/jwt-claims/verify/schemas/ctx/procedures/security_definer.sql +++ b/packages/jwt-claims/verify/schemas/ctx/procedures/security_definer.sql @@ -2,8 +2,7 @@ BEGIN; -SELECT verify_function ('ctx.security_definer'); -SELECT verify_function ('ctx.is_security_definer'); +SELECT assert_function('ctx.security_definer()'::regprocedure, 'text'::regtype); +SELECT assert_function('ctx.is_security_definer()'::regprocedure, 'boolean'::regtype); ROLLBACK; - diff --git a/packages/jwt-claims/verify/schemas/ctx/procedures/uagent.sql b/packages/jwt-claims/verify/schemas/ctx/procedures/uagent.sql index 917fa5054..1a2b27fe1 100644 --- a/packages/jwt-claims/verify/schemas/ctx/procedures/uagent.sql +++ b/packages/jwt-claims/verify/schemas/ctx/procedures/uagent.sql @@ -2,7 +2,7 @@ BEGIN; -SELECT verify_function ('ctx.uagent'); +SELECT assert_function('ctx.uagent()'::regprocedure); ROLLBACK; diff --git a/packages/jwt-claims/verify/schemas/ctx/procedures/uid.sql b/packages/jwt-claims/verify/schemas/ctx/procedures/uid.sql index 82d56cdec..dd10209c4 100644 --- a/packages/jwt-claims/verify/schemas/ctx/procedures/uid.sql +++ b/packages/jwt-claims/verify/schemas/ctx/procedures/uid.sql @@ -2,7 +2,7 @@ BEGIN; -SELECT verify_function ('ctx.uid'); +SELECT assert_function('ctx.uid()'::regprocedure); ROLLBACK; diff --git a/packages/jwt-claims/verify/schemas/ctx/schema.sql b/packages/jwt-claims/verify/schemas/ctx/schema.sql index 297e87c37..487466087 100644 --- a/packages/jwt-claims/verify/schemas/ctx/schema.sql +++ b/packages/jwt-claims/verify/schemas/ctx/schema.sql @@ -2,7 +2,7 @@ BEGIN; -SELECT verify_schema ('ctx'); +SELECT assert_schema('ctx'::regnamespace); ROLLBACK; diff --git a/packages/jwt-claims/verify/schemas/jwt_private/procedures/current_api_id.sql b/packages/jwt-claims/verify/schemas/jwt_private/procedures/current_api_id.sql index 6cd2484e2..b59a9ce7d 100644 --- a/packages/jwt-claims/verify/schemas/jwt_private/procedures/current_api_id.sql +++ b/packages/jwt-claims/verify/schemas/jwt_private/procedures/current_api_id.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('jwt_private.current_api_id'); +SELECT assert_function('jwt_private.current_api_id()'::regprocedure); ROLLBACK; diff --git a/packages/jwt-claims/verify/schemas/jwt_private/procedures/current_database_id.sql b/packages/jwt-claims/verify/schemas/jwt_private/procedures/current_database_id.sql index 33311a617..c7d61af59 100644 --- a/packages/jwt-claims/verify/schemas/jwt_private/procedures/current_database_id.sql +++ b/packages/jwt-claims/verify/schemas/jwt_private/procedures/current_database_id.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('jwt_private.current_database_id'); +SELECT assert_function('jwt_private.current_database_id()'::regprocedure); ROLLBACK; diff --git a/packages/jwt-claims/verify/schemas/jwt_private/procedures/current_graph_execution_id.sql b/packages/jwt-claims/verify/schemas/jwt_private/procedures/current_graph_execution_id.sql index b1a8bd583..a2902a2a8 100644 --- a/packages/jwt-claims/verify/schemas/jwt_private/procedures/current_graph_execution_id.sql +++ b/packages/jwt-claims/verify/schemas/jwt_private/procedures/current_graph_execution_id.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('jwt_private.current_graph_execution_id'); +SELECT assert_function('jwt_private.current_graph_execution_id()'::regprocedure); ROLLBACK; diff --git a/packages/jwt-claims/verify/schemas/jwt_private/procedures/current_session_id.sql b/packages/jwt-claims/verify/schemas/jwt_private/procedures/current_session_id.sql index 8eebd8340..73b2b287d 100644 --- a/packages/jwt-claims/verify/schemas/jwt_private/procedures/current_session_id.sql +++ b/packages/jwt-claims/verify/schemas/jwt_private/procedures/current_session_id.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('jwt_private.current_session_id'); +SELECT assert_function('jwt_private.current_session_id()'::regprocedure); ROLLBACK; diff --git a/packages/jwt-claims/verify/schemas/jwt_private/procedures/current_token_id.sql b/packages/jwt-claims/verify/schemas/jwt_private/procedures/current_token_id.sql index 756b4b54e..b84fc660d 100644 --- a/packages/jwt-claims/verify/schemas/jwt_private/procedures/current_token_id.sql +++ b/packages/jwt-claims/verify/schemas/jwt_private/procedures/current_token_id.sql @@ -2,7 +2,7 @@ BEGIN; -SELECT verify_function ('jwt_private.current_token_id'); +SELECT assert_function('jwt_private.current_token_id()'::regprocedure); ROLLBACK; diff --git a/packages/jwt-claims/verify/schemas/jwt_private/schema.sql b/packages/jwt-claims/verify/schemas/jwt_private/schema.sql index 8dc051d18..da7f8d8ca 100644 --- a/packages/jwt-claims/verify/schemas/jwt_private/schema.sql +++ b/packages/jwt-claims/verify/schemas/jwt_private/schema.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_schema ('jwt_private'); +SELECT assert_schema('jwt_private'::regnamespace); ROLLBACK; diff --git a/packages/jwt-claims/verify/schemas/jwt_public/procedures/current_ip_address.sql b/packages/jwt-claims/verify/schemas/jwt_public/procedures/current_ip_address.sql index bce457e25..d4be89b79 100644 --- a/packages/jwt-claims/verify/schemas/jwt_public/procedures/current_ip_address.sql +++ b/packages/jwt-claims/verify/schemas/jwt_public/procedures/current_ip_address.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('jwt_public.current_ip_address'); +SELECT assert_function('jwt_public.current_ip_address()'::regprocedure); ROLLBACK; diff --git a/packages/jwt-claims/verify/schemas/jwt_public/procedures/current_origin.sql b/packages/jwt-claims/verify/schemas/jwt_public/procedures/current_origin.sql index d1605a9ab..af9d63f92 100644 --- a/packages/jwt-claims/verify/schemas/jwt_public/procedures/current_origin.sql +++ b/packages/jwt-claims/verify/schemas/jwt_public/procedures/current_origin.sql @@ -2,7 +2,7 @@ BEGIN; -SELECT verify_function ('jwt_public.current_origin'); +SELECT assert_function('jwt_public.current_origin()'::regprocedure); ROLLBACK; diff --git a/packages/jwt-claims/verify/schemas/jwt_public/procedures/current_principal_id.sql b/packages/jwt-claims/verify/schemas/jwt_public/procedures/current_principal_id.sql index 911d75c6c..2106aa65b 100644 --- a/packages/jwt-claims/verify/schemas/jwt_public/procedures/current_principal_id.sql +++ b/packages/jwt-claims/verify/schemas/jwt_public/procedures/current_principal_id.sql @@ -2,9 +2,6 @@ BEGIN; -SELECT has_function_privilege( - 'jwt_public.current_principal_id()', - 'execute' -); +SELECT assert_function('jwt_public.current_principal_id()'::regprocedure); ROLLBACK; diff --git a/packages/jwt-claims/verify/schemas/jwt_public/procedures/current_role_type.sql b/packages/jwt-claims/verify/schemas/jwt_public/procedures/current_role_type.sql index dd0c9fbad..287beeef8 100644 --- a/packages/jwt-claims/verify/schemas/jwt_public/procedures/current_role_type.sql +++ b/packages/jwt-claims/verify/schemas/jwt_public/procedures/current_role_type.sql @@ -2,9 +2,6 @@ BEGIN; -SELECT has_function_privilege( - 'jwt_public.current_role_type()', - 'execute' -); +SELECT assert_function('jwt_public.current_role_type()'::regprocedure); ROLLBACK; diff --git a/packages/jwt-claims/verify/schemas/jwt_public/procedures/current_user_agent.sql b/packages/jwt-claims/verify/schemas/jwt_public/procedures/current_user_agent.sql index e807ae301..3fc7200f2 100644 --- a/packages/jwt-claims/verify/schemas/jwt_public/procedures/current_user_agent.sql +++ b/packages/jwt-claims/verify/schemas/jwt_public/procedures/current_user_agent.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('jwt_public.current_user_agent'); +SELECT assert_function('jwt_public.current_user_agent()'::regprocedure); ROLLBACK; diff --git a/packages/jwt-claims/verify/schemas/jwt_public/procedures/current_user_id.sql b/packages/jwt-claims/verify/schemas/jwt_public/procedures/current_user_id.sql index 82a757bf6..6e4427edc 100644 --- a/packages/jwt-claims/verify/schemas/jwt_public/procedures/current_user_id.sql +++ b/packages/jwt-claims/verify/schemas/jwt_public/procedures/current_user_id.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('jwt_public.current_user_id'); +SELECT assert_function('jwt_public.current_user_id()'::regprocedure); ROLLBACK; diff --git a/packages/jwt-claims/verify/schemas/jwt_public/schema.sql b/packages/jwt-claims/verify/schemas/jwt_public/schema.sql index 1a9c5105b..947848f57 100644 --- a/packages/jwt-claims/verify/schemas/jwt_public/schema.sql +++ b/packages/jwt-claims/verify/schemas/jwt_public/schema.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_schema ('jwt_public'); +SELECT assert_schema('jwt_public'::regnamespace); ROLLBACK; diff --git a/packages/ltree-helpers/Makefile b/packages/ltree-helpers/Makefile index addb87623..b18e8b966 100644 --- a/packages/ltree-helpers/Makefile +++ b/packages/ltree-helpers/Makefile @@ -1,5 +1,5 @@ EXTENSION = pgpm-ltree-helpers -DATA = sql/pgpm-ltree-helpers--0.36.0.sql +DATA = sql/pgpm-ltree-helpers--0.39.0.sql PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) diff --git a/packages/ltree-helpers/package.json b/packages/ltree-helpers/package.json index 7eb16b7f9..4bd6f6d92 100644 --- a/packages/ltree-helpers/package.json +++ b/packages/ltree-helpers/package.json @@ -1,6 +1,6 @@ { "name": "@pgpm/ltree-helpers", - "version": "0.36.0", + "version": "0.39.0", "description": "Slash-path to ltree/lquery conversion helpers for PostgreSQL", "author": "Dan Lynch ", "contributors": [ diff --git a/packages/ltree-helpers/pgpm-ltree-helpers.control b/packages/ltree-helpers/pgpm-ltree-helpers.control index 5a2e0d350..fe67ac631 100644 --- a/packages/ltree-helpers/pgpm-ltree-helpers.control +++ b/packages/ltree-helpers/pgpm-ltree-helpers.control @@ -1,6 +1,6 @@ # pgpm-ltree-helpers extension comment = 'pgpm-ltree-helpers extension' -default_version = '0.36.0' +default_version = '0.39.0' module_pathname = '$libdir/pgpm-ltree-helpers' requires = 'plpgsql,ltree,pgpm-verify' relocatable = false diff --git a/packages/ltree-helpers/revert/schemas/ltree_helpers/procedures/to_path.sql b/packages/ltree-helpers/revert/schemas/ltree_helpers/procedures/to_path.sql index 9cf9d89f6..7b0b502c2 100644 --- a/packages/ltree-helpers/revert/schemas/ltree_helpers/procedures/to_path.sql +++ b/packages/ltree-helpers/revert/schemas/ltree_helpers/procedures/to_path.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION ltree_helpers.to_path; +DROP FUNCTION ltree_helpers.to_path(text); COMMIT; diff --git a/packages/ltree-helpers/revert/schemas/ltree_helpers/procedures/to_query.sql b/packages/ltree-helpers/revert/schemas/ltree_helpers/procedures/to_query.sql index cc81a9dfd..55c238544 100644 --- a/packages/ltree-helpers/revert/schemas/ltree_helpers/procedures/to_query.sql +++ b/packages/ltree-helpers/revert/schemas/ltree_helpers/procedures/to_query.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION ltree_helpers.to_query; +DROP FUNCTION ltree_helpers.to_query(text); COMMIT; diff --git a/packages/ltree-helpers/revert/schemas/ltree_helpers/procedures/to_slash.sql b/packages/ltree-helpers/revert/schemas/ltree_helpers/procedures/to_slash.sql index d2fee251c..b4f4027bc 100644 --- a/packages/ltree-helpers/revert/schemas/ltree_helpers/procedures/to_slash.sql +++ b/packages/ltree-helpers/revert/schemas/ltree_helpers/procedures/to_slash.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION ltree_helpers.to_slash; +DROP FUNCTION ltree_helpers.to_slash(ltree); COMMIT; diff --git a/packages/ltree-helpers/sql/pgpm-ltree-helpers--0.36.0.bundle.tar.gz b/packages/ltree-helpers/sql/pgpm-ltree-helpers--0.36.0.bundle.tar.gz deleted file mode 100644 index ccb7187ad..000000000 Binary files a/packages/ltree-helpers/sql/pgpm-ltree-helpers--0.36.0.bundle.tar.gz and /dev/null differ diff --git a/packages/ltree-helpers/sql/pgpm-ltree-helpers--0.39.0.bundle.tar.gz b/packages/ltree-helpers/sql/pgpm-ltree-helpers--0.39.0.bundle.tar.gz new file mode 100644 index 000000000..00f43174b Binary files /dev/null and b/packages/ltree-helpers/sql/pgpm-ltree-helpers--0.39.0.bundle.tar.gz differ diff --git a/packages/ltree-helpers/sql/pgpm-ltree-helpers--0.36.0.sql b/packages/ltree-helpers/sql/pgpm-ltree-helpers--0.39.0.sql similarity index 100% rename from packages/ltree-helpers/sql/pgpm-ltree-helpers--0.36.0.sql rename to packages/ltree-helpers/sql/pgpm-ltree-helpers--0.39.0.sql diff --git a/packages/ltree-helpers/verify/schemas/ltree_helpers/procedures/to_path.sql b/packages/ltree-helpers/verify/schemas/ltree_helpers/procedures/to_path.sql index 28a480f4e..6a0fbdbce 100644 --- a/packages/ltree-helpers/verify/schemas/ltree_helpers/procedures/to_path.sql +++ b/packages/ltree-helpers/verify/schemas/ltree_helpers/procedures/to_path.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('ltree_helpers.to_path'); +SELECT assert_function('ltree_helpers.to_path(text)'::regprocedure); ROLLBACK; diff --git a/packages/ltree-helpers/verify/schemas/ltree_helpers/procedures/to_query.sql b/packages/ltree-helpers/verify/schemas/ltree_helpers/procedures/to_query.sql index ad2e13056..8668f4fa7 100644 --- a/packages/ltree-helpers/verify/schemas/ltree_helpers/procedures/to_query.sql +++ b/packages/ltree-helpers/verify/schemas/ltree_helpers/procedures/to_query.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('ltree_helpers.to_query'); +SELECT assert_function('ltree_helpers.to_query(text)'::regprocedure); ROLLBACK; diff --git a/packages/ltree-helpers/verify/schemas/ltree_helpers/procedures/to_slash.sql b/packages/ltree-helpers/verify/schemas/ltree_helpers/procedures/to_slash.sql index 2957c7014..901048658 100644 --- a/packages/ltree-helpers/verify/schemas/ltree_helpers/procedures/to_slash.sql +++ b/packages/ltree-helpers/verify/schemas/ltree_helpers/procedures/to_slash.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('ltree_helpers.to_slash'); +SELECT assert_function('ltree_helpers.to_slash(ltree)'::regprocedure); ROLLBACK; diff --git a/packages/ltree-helpers/verify/schemas/ltree_helpers/schema.sql b/packages/ltree-helpers/verify/schemas/ltree_helpers/schema.sql index 9f8dbe338..a78fdcaed 100644 --- a/packages/ltree-helpers/verify/schemas/ltree_helpers/schema.sql +++ b/packages/ltree-helpers/verify/schemas/ltree_helpers/schema.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_schema ('ltree_helpers'); +SELECT assert_schema('ltree_helpers'::regnamespace); ROLLBACK; diff --git a/packages/measurements/Makefile b/packages/measurements/Makefile index 08c732718..4b9239179 100644 --- a/packages/measurements/Makefile +++ b/packages/measurements/Makefile @@ -1,5 +1,5 @@ EXTENSION = pgpm-measurements -DATA = sql/pgpm-measurements--0.36.0.sql +DATA = sql/pgpm-measurements--0.39.0.sql PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) diff --git a/packages/measurements/package.json b/packages/measurements/package.json index 8142f9955..164de2a17 100644 --- a/packages/measurements/package.json +++ b/packages/measurements/package.json @@ -1,6 +1,6 @@ { "name": "@pgpm/measurements", - "version": "0.36.0", + "version": "0.39.0", "description": "Measurement utilities for performance tracking and analytics", "author": "Dan Lynch ", "contributors": [ diff --git a/packages/measurements/pgpm-measurements.control b/packages/measurements/pgpm-measurements.control index 625b73f2c..f8e58bd83 100644 --- a/packages/measurements/pgpm-measurements.control +++ b/packages/measurements/pgpm-measurements.control @@ -1,6 +1,6 @@ # pgpm-measurements extension comment = 'pgpm-measurements extension' -default_version = '0.36.0' +default_version = '0.39.0' module_pathname = '$libdir/pgpm-measurements' requires = 'plpgsql,pgpm-verify' relocatable = false diff --git a/packages/measurements/sql/pgpm-measurements--0.36.0.bundle.tar.gz b/packages/measurements/sql/pgpm-measurements--0.36.0.bundle.tar.gz deleted file mode 100644 index 05629930e..000000000 Binary files a/packages/measurements/sql/pgpm-measurements--0.36.0.bundle.tar.gz and /dev/null differ diff --git a/packages/measurements/sql/pgpm-measurements--0.39.0.bundle.tar.gz b/packages/measurements/sql/pgpm-measurements--0.39.0.bundle.tar.gz new file mode 100644 index 000000000..eea32f7f0 Binary files /dev/null and b/packages/measurements/sql/pgpm-measurements--0.39.0.bundle.tar.gz differ diff --git a/packages/measurements/sql/pgpm-measurements--0.36.0.sql b/packages/measurements/sql/pgpm-measurements--0.39.0.sql similarity index 100% rename from packages/measurements/sql/pgpm-measurements--0.36.0.sql rename to packages/measurements/sql/pgpm-measurements--0.39.0.sql diff --git a/packages/measurements/verify/schemas/measurements/schema.sql b/packages/measurements/verify/schemas/measurements/schema.sql index b53cd7c56..dbef3da40 100644 --- a/packages/measurements/verify/schemas/measurements/schema.sql +++ b/packages/measurements/verify/schemas/measurements/schema.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_schema ('measurements'); +SELECT assert_schema('measurements'::regnamespace); ROLLBACK; diff --git a/packages/measurements/verify/schemas/measurements/tables/quantities/table.sql b/packages/measurements/verify/schemas/measurements/tables/quantities/table.sql index fd6b10a78..b6ff3ff93 100644 --- a/packages/measurements/verify/schemas/measurements/tables/quantities/table.sql +++ b/packages/measurements/verify/schemas/measurements/tables/quantities/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('measurements.quantities'); +SELECT assert_table('measurements.quantities'::regclass); ROLLBACK; diff --git a/packages/metaschema-modules/Makefile b/packages/metaschema-modules/Makefile index 1fcecbd4f..116e52db3 100644 --- a/packages/metaschema-modules/Makefile +++ b/packages/metaschema-modules/Makefile @@ -1,5 +1,5 @@ EXTENSION = metaschema-modules -DATA = sql/metaschema-modules--0.36.0.sql +DATA = sql/metaschema-modules--0.39.0.sql PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) diff --git a/packages/metaschema-modules/__tests__/__snapshots__/modules.test.ts.snap b/packages/metaschema-modules/__tests__/__snapshots__/modules.test.ts.snap index 5dd7b91d2..aaa56b074 100644 --- a/packages/metaschema-modules/__tests__/__snapshots__/modules.test.ts.snap +++ b/packages/metaschema-modules/__tests__/__snapshots__/modules.test.ts.snap @@ -55,6 +55,7 @@ exports[`db_meta_modules should have all expected module tables 1`] = ` "resource_module", "rls_module", "route_module", + "scope_types_module", "session_secrets_module", "sessions_module", "site_surface_module", @@ -64,6 +65,7 @@ exports[`db_meta_modules should have all expected module tables 1`] = ` "user_auth_module", "user_credentials_module", "user_settings_module", + "user_settings_security_module", "user_state_module", "users_module", "webauthn_auth_module", @@ -75,8 +77,8 @@ exports[`db_meta_modules should have all expected module tables 1`] = ` exports[`db_meta_modules should verify all module tables exist in metaschema_modules_public schema 1`] = ` { - "moduleTablesCount": 66, - "totalTables": 73, + "moduleTablesCount": 68, + "totalTables": 75, } `; @@ -143,13 +145,13 @@ exports[`db_meta_modules should verify emails_module table structure 1`] = ` exports[`db_meta_modules should verify module table structures have database_id foreign keys 1`] = ` { - "constraintCount": 66, + "constraintCount": 68, } `; exports[`db_meta_modules should verify module tables have proper foreign key relationships 1`] = ` { - "constraintCount": 476, + "constraintCount": 486, "foreignTables": [ "catalog_module", "database", diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/billing_provider_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/billing_provider_module/table.sql index 4f18e53fa..8784d1f92 100644 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/billing_provider_module/table.sql +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/billing_provider_module/table.sql @@ -37,8 +37,20 @@ CREATE TABLE metaschema_modules_public.billing_provider_module ( billing_webhook_events_table_id uuid NOT NULL DEFAULT uuid_nil(), billing_webhook_events_table_name text NOT NULL DEFAULT '', + billing_refunds_table_id uuid NOT NULL DEFAULT uuid_nil(), + billing_refunds_table_name text NOT NULL DEFAULT '', + + billing_invoices_table_id uuid NOT NULL DEFAULT uuid_nil(), + billing_invoices_table_name text NOT NULL DEFAULT '', + -- Generated functions process_billing_event_function text NOT NULL DEFAULT '', + record_refund_function text NOT NULL DEFAULT '', + upsert_invoice_function text NOT NULL DEFAULT '', + -- Usage sync lives here rather than on billing_module: reporting usage needs + -- the provider's subscription item, which only this module knows about. + list_pending_usage_sync_function text NOT NULL DEFAULT '', + mark_usage_synced_function text NOT NULL DEFAULT '', prefix text NULL, @@ -54,6 +66,8 @@ CREATE TABLE metaschema_modules_public.billing_provider_module ( CONSTRAINT billing_prices_table_fkey FOREIGN KEY (billing_prices_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT billing_subscriptions_table_fkey FOREIGN KEY (billing_subscriptions_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT billing_webhook_events_table_fkey FOREIGN KEY (billing_webhook_events_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT billing_refunds_table_fkey FOREIGN KEY (billing_refunds_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT billing_invoices_table_fkey FOREIGN KEY (billing_invoices_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT products_table_fkey FOREIGN KEY (products_table_id) REFERENCES metaschema_public.table (id) ON DELETE SET NULL, CONSTRAINT prices_table_fkey FOREIGN KEY (prices_table_id) REFERENCES metaschema_public.table (id) ON DELETE SET NULL, CONSTRAINT subscriptions_table_fkey FOREIGN KEY (subscriptions_table_id) REFERENCES metaschema_public.table (id) ON DELETE SET NULL, @@ -65,6 +79,8 @@ CREATE INDEX billing_provider_module_billing_prices_table_id_idx ON metaschema_m CREATE INDEX billing_provider_module_billing_products_table_id_idx ON metaschema_modules_public.billing_provider_module ( billing_products_table_id ); CREATE INDEX billing_provider_module_billing_subscriptions_table_id_idx ON metaschema_modules_public.billing_provider_module ( billing_subscriptions_table_id ); CREATE INDEX billing_provider_module_billing_webhook_events_table_id_idx ON metaschema_modules_public.billing_provider_module ( billing_webhook_events_table_id ); +CREATE INDEX billing_provider_module_billing_refunds_table_id_idx ON metaschema_modules_public.billing_provider_module ( billing_refunds_table_id ); +CREATE INDEX billing_provider_module_billing_invoices_table_id_idx ON metaschema_modules_public.billing_provider_module ( billing_invoices_table_id ); CREATE INDEX billing_provider_module_prices_table_id_idx ON metaschema_modules_public.billing_provider_module ( prices_table_id ); CREATE INDEX billing_provider_module_products_table_id_idx ON metaschema_modules_public.billing_provider_module ( products_table_id ); CREATE INDEX billing_provider_module_subscriptions_table_id_idx ON metaschema_modules_public.billing_provider_module ( subscriptions_table_id ); diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/catalog_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/catalog_module/table.sql index d0f0bacfc..6e179049a 100644 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/catalog_module/table.sql +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/catalog_module/table.sql @@ -5,9 +5,9 @@ BEGIN; -- Typed catalog module configuration: one row per database installs the typed --- catalog tables (catalog_public.domains / apis / sites / namespaces / +-- catalog tables (catalog_private.domains / apis / sites / namespaces / -- functions / resources / resource_definitions / resource_installations / --- apps / buckets). The catalog is +-- apps / buckets / bindings). The catalog is -- a system projection surface holding ALL scopes of each type; scoped source -- tables register into it via catalog_register. The stable schema-qualified -- table names are load-bearing deployment contracts. @@ -32,6 +32,7 @@ CREATE TABLE metaschema_modules_public.catalog_module ( resource_installations_table_id uuid NOT NULL DEFAULT uuid_nil(), apps_table_id uuid NOT NULL DEFAULT uuid_nil(), buckets_table_id uuid NOT NULL DEFAULT uuid_nil(), + bindings_table_id uuid NOT NULL DEFAULT uuid_nil(), sites_web_config_table_id uuid NOT NULL DEFAULT uuid_nil(), sites_error_pages_table_id uuid NOT NULL DEFAULT uuid_nil(), sites_app_links_table_id uuid NOT NULL DEFAULT uuid_nil(), @@ -48,6 +49,7 @@ CREATE TABLE metaschema_modules_public.catalog_module ( resource_installations_table_name text NOT NULL DEFAULT 'resource_installations', apps_table_name text NOT NULL DEFAULT 'apps', buckets_table_name text NOT NULL DEFAULT 'buckets', + bindings_table_name text NOT NULL DEFAULT 'bindings', sites_web_config_table_name text NOT NULL DEFAULT 'sites_web_config', sites_error_pages_table_name text NOT NULL DEFAULT 'sites_error_pages', sites_app_links_table_name text NOT NULL DEFAULT 'sites_app_links', @@ -121,6 +123,10 @@ CREATE TABLE metaschema_modules_public.catalog_module ( FOREIGN KEY (buckets_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT catalog_module_bindings_table_fkey + FOREIGN KEY (bindings_table_id) + REFERENCES metaschema_public.table (id) + ON DELETE CASCADE, CONSTRAINT catalog_module_sites_web_config_table_fkey FOREIGN KEY (sites_web_config_table_id) REFERENCES metaschema_public.table (id) @@ -150,6 +156,7 @@ CREATE UNIQUE INDEX catalog_module_unique_database CREATE INDEX catalog_module_apis_table_id_idx ON metaschema_modules_public.catalog_module ( apis_table_id ); CREATE INDEX catalog_module_apps_table_id_idx ON metaschema_modules_public.catalog_module ( apps_table_id ); CREATE INDEX catalog_module_buckets_table_id_idx ON metaschema_modules_public.catalog_module ( buckets_table_id ); +CREATE INDEX catalog_module_bindings_table_id_idx ON metaschema_modules_public.catalog_module ( bindings_table_id ); CREATE INDEX catalog_module_sites_web_config_table_id_idx ON metaschema_modules_public.catalog_module ( sites_web_config_table_id ); CREATE INDEX catalog_module_sites_error_pages_table_id_idx ON metaschema_modules_public.catalog_module ( sites_error_pages_table_id ); CREATE INDEX catalog_module_sites_app_links_table_id_idx ON metaschema_modules_public.catalog_module ( sites_app_links_table_id ); diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/entity_type_provision/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/entity_type_provision/table.sql index 14e1a7585..e2665f921 100644 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/entity_type_provision/table.sql +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/entity_type_provision/table.sql @@ -85,7 +85,7 @@ CREATE TABLE metaschema_modules_public.entity_type_provision ( -- ========================================================================= -- Table provisioning override: single jsonb object describing the full -- security setup to apply to the entity table, using the same vocabulary - -- as metaschema_modules_public.provision_table() and blueprint tables[] + -- as metaschema_modules_private.provision_table() and blueprint tables[] -- entries (policies[], nodes[], fields[], grants[], use_rls). -- -- Semantics: @@ -276,7 +276,7 @@ COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.skip_entity_po COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.table_provision IS 'Single jsonb object describing the full security setup to apply to the entity table. - Uses the same vocabulary as metaschema_modules_public.provision_table() and blueprint tables[] + Uses the same vocabulary as metaschema_modules_private.provision_table() and blueprint tables[] entries, so an entity table is configured the same way an ordinary blueprint table is. Defaults to NULL; when non-NULL, the five default policies are implicitly replaced by table_provision.policies[] (is_visible becomes a no-op on this path). diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/scope_types_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/scope_types_module/table.sql new file mode 100644 index 000000000..02953ff50 --- /dev/null +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/scope_types_module/table.sql @@ -0,0 +1,38 @@ +-- Deploy schemas/metaschema_modules_public/tables/scope_types_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +-- Config row for the scope type projection: the global table that records, for +-- every scope of every logical database, which scope encloses it. +-- +-- One row per POSTGRES database, provisioned once from the platform database's +-- plane pass beside catalog_module — the typed catalog's shape. A plane read by +-- static SQL cannot be generated per logical database: metaschema prefixes each +-- generated schema with the owning database's schema_hash, and no static reader +-- can name a hashed schema. +CREATE TABLE metaschema_modules_public.scope_types_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + -- + schema_id uuid NOT NULL DEFAULT uuid_nil(), + + -- Schema name override: when set, the trigger uses this instead of the + -- 'scope_private' default. + private_schema_name text, + + scope_types_table_id uuid NOT NULL DEFAULT uuid_nil(), + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT scope_types_table_fkey FOREIGN KEY (scope_types_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + + CONSTRAINT scope_types_module_unique UNIQUE (database_id) +); + +CREATE INDEX scope_types_module_database_id_idx ON metaschema_modules_public.scope_types_module ( database_id ); +CREATE INDEX scope_types_module_schema_id_idx ON metaschema_modules_public.scope_types_module ( schema_id ); +CREATE INDEX scope_types_module_scope_types_table_id_idx ON metaschema_modules_public.scope_types_module ( scope_types_table_id ); + +COMMIT; diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/user_settings_security_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/user_settings_security_module/table.sql new file mode 100644 index 000000000..c0254ca11 --- /dev/null +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/user_settings_security_module/table.sql @@ -0,0 +1,36 @@ +-- Deploy schemas/metaschema_modules_public/tables/user_settings_security_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.user_settings_security_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + -- Schema reference (populated by the insert trigger) + schema_id uuid NOT NULL DEFAULT uuid_nil(), + + -- Table reference (populated by the generator) + table_id uuid NOT NULL DEFAULT uuid_nil(), + + -- Owner table reference (resolved to users table by trigger) + owner_table_id uuid NOT NULL DEFAULT uuid_nil(), + + table_name text NOT NULL DEFAULT 'user_settings_security', + + -- API routing (configurable per-module) + api_name text DEFAULT NULL, + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT owner_table_fkey FOREIGN KEY (owner_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE +); + +CREATE UNIQUE INDEX user_settings_security_module_unique_per_db ON metaschema_modules_public.user_settings_security_module ( database_id ); +CREATE INDEX user_settings_security_module_owner_table_id_idx ON metaschema_modules_public.user_settings_security_module ( owner_table_id ); +CREATE INDEX user_settings_security_module_table_id_idx ON metaschema_modules_public.user_settings_security_module ( table_id ); +CREATE INDEX user_settings_security_module_schema_id_idx ON metaschema_modules_public.user_settings_security_module ( schema_id ); + +COMMIT; diff --git a/packages/metaschema-modules/metaschema-modules.control b/packages/metaschema-modules/metaschema-modules.control index e7c89017b..dd2734d4c 100644 --- a/packages/metaschema-modules/metaschema-modules.control +++ b/packages/metaschema-modules/metaschema-modules.control @@ -1,6 +1,6 @@ # metaschema-modules extension comment = 'metaschema-modules extension' -default_version = '0.36.0' +default_version = '0.39.0' module_pathname = '$libdir/metaschema-modules' requires = 'plpgsql,uuid-ossp,metaschema-schema,pgpm-verify' relocatable = false diff --git a/packages/metaschema-modules/package.json b/packages/metaschema-modules/package.json index 306624da1..469a63d00 100644 --- a/packages/metaschema-modules/package.json +++ b/packages/metaschema-modules/package.json @@ -1,6 +1,6 @@ { "name": "@pgpm/metaschema-modules", - "version": "0.36.0", + "version": "0.39.0", "description": "Module metadata handling and dependency tracking", "author": "Dan Lynch ", "contributors": [ diff --git a/packages/metaschema-modules/pgpm.plan b/packages/metaschema-modules/pgpm.plan index 1d46d9793..dd490eba1 100644 --- a/packages/metaschema-modules/pgpm.plan +++ b/packages/metaschema-modules/pgpm.plan @@ -87,3 +87,5 @@ schemas/metaschema_modules_public/tables/route_module/table [schemas/metaschema_ schemas/metaschema_modules_public/tables/app_module/table [schemas/metaschema_modules_public/schema schemas/metaschema_modules_public/tables/catalog_module/table] 2026-07-19T14:00:00Z devin # add app aggregate module config schemas/metaschema_modules_public/tables/database_settings_module/table [schemas/metaschema_modules_public/schema] 2026-07-20T21:00:00Z devin # add scoped database settings module config (database/rls/pubkey/webauthn settings) schemas/metaschema_modules_public/tables/pages_module/table [schemas/metaschema_modules_public/schema schemas/metaschema_modules_public/tables/merkle_store_module/table schemas/metaschema_modules_public/tables/site_surface_module/table] 2026-07-31T19:00:00Z devin # add pages_module config table for site-owned merkle-versioned page content +schemas/metaschema_modules_public/tables/user_settings_security_module/table [schemas/metaschema_modules_public/schema] 2026-08-05T00:00:00Z devin # add user_settings_security_module for per-user MFA enrollment state (1:1 with users) +schemas/metaschema_modules_public/tables/scope_types_module/table [schemas/metaschema_modules_public/schema] 2026-08-06T00:00:00Z devin # add scope type projection module config (which scope encloses each scope) diff --git a/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/scope_types_module/table.sql b/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/scope_types_module/table.sql new file mode 100644 index 000000000..6f4f31e8f --- /dev/null +++ b/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/scope_types_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/scope_types_module/table from pg + +BEGIN; + +DROP TABLE metaschema_modules_public.scope_types_module; + +COMMIT; diff --git a/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/user_settings_security_module/table.sql b/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/user_settings_security_module/table.sql new file mode 100644 index 000000000..0be6b993e --- /dev/null +++ b/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/user_settings_security_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/user_settings_security_module/table from pg + +BEGIN; + +DROP TABLE metaschema_modules_public.user_settings_security_module; + +COMMIT; diff --git a/packages/metaschema-modules/sql/metaschema-modules--0.36.0.bundle.tar.gz b/packages/metaschema-modules/sql/metaschema-modules--0.36.0.bundle.tar.gz deleted file mode 100644 index 077c33166..000000000 Binary files a/packages/metaschema-modules/sql/metaschema-modules--0.36.0.bundle.tar.gz and /dev/null differ diff --git a/packages/metaschema-modules/sql/metaschema-modules--0.39.0.bundle.tar.gz b/packages/metaschema-modules/sql/metaschema-modules--0.39.0.bundle.tar.gz new file mode 100644 index 000000000..69a67b104 Binary files /dev/null and b/packages/metaschema-modules/sql/metaschema-modules--0.39.0.bundle.tar.gz differ diff --git a/packages/metaschema-modules/sql/metaschema-modules--0.36.0.sql b/packages/metaschema-modules/sql/metaschema-modules--0.39.0.sql similarity index 98% rename from packages/metaschema-modules/sql/metaschema-modules--0.36.0.sql rename to packages/metaschema-modules/sql/metaschema-modules--0.39.0.sql index 2a46f502c..c5a0a76ae 100644 --- a/packages/metaschema-modules/sql/metaschema-modules--0.36.0.sql +++ b/packages/metaschema-modules/sql/metaschema-modules--0.39.0.sql @@ -1961,7 +1961,7 @@ COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.skip_entity_po - DELETE: owner of the entity can delete it'; COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.table_provision IS 'Single jsonb object describing the full security setup to apply to the entity table. - Uses the same vocabulary as metaschema_modules_public.provision_table() and blueprint tables[] + Uses the same vocabulary as metaschema_modules_private.provision_table() and blueprint tables[] entries, so an entity table is configured the same way an ordinary blueprint table is. Defaults to NULL; when non-NULL, the five default policies are implicitly replaced by table_provision.policies[] (is_visible becomes a no-op on this path). @@ -2695,7 +2695,15 @@ CREATE TABLE metaschema_modules_public.billing_provider_module ( billing_subscriptions_table_name text NOT NULL DEFAULT '', billing_webhook_events_table_id uuid NOT NULL DEFAULT uuid_nil(), billing_webhook_events_table_name text NOT NULL DEFAULT '', + billing_refunds_table_id uuid NOT NULL DEFAULT uuid_nil(), + billing_refunds_table_name text NOT NULL DEFAULT '', + billing_invoices_table_id uuid NOT NULL DEFAULT uuid_nil(), + billing_invoices_table_name text NOT NULL DEFAULT '', process_billing_event_function text NOT NULL DEFAULT '', + record_refund_function text NOT NULL DEFAULT '', + upsert_invoice_function text NOT NULL DEFAULT '', + list_pending_usage_sync_function text NOT NULL DEFAULT '', + mark_usage_synced_function text NOT NULL DEFAULT '', prefix text NULL, api_name text DEFAULT NULL, private_api_name text DEFAULT NULL, @@ -2731,6 +2739,14 @@ CREATE TABLE metaschema_modules_public.billing_provider_module ( FOREIGN KEY(billing_webhook_events_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT billing_refunds_table_fkey + FOREIGN KEY(billing_refunds_table_id) + REFERENCES metaschema_public.table (id) + ON DELETE CASCADE, + CONSTRAINT billing_invoices_table_fkey + FOREIGN KEY(billing_invoices_table_id) + REFERENCES metaschema_public.table (id) + ON DELETE CASCADE, CONSTRAINT products_table_fkey FOREIGN KEY(products_table_id) REFERENCES metaschema_public.table (id) @@ -2757,6 +2773,10 @@ CREATE INDEX billing_provider_module_billing_subscriptions_table_id_idx ON metas CREATE INDEX billing_provider_module_billing_webhook_events_table_id_idx ON metaschema_modules_public.billing_provider_module (billing_webhook_events_table_id); +CREATE INDEX billing_provider_module_billing_refunds_table_id_idx ON metaschema_modules_public.billing_provider_module (billing_refunds_table_id); + +CREATE INDEX billing_provider_module_billing_invoices_table_id_idx ON metaschema_modules_public.billing_provider_module (billing_invoices_table_id); + CREATE INDEX billing_provider_module_prices_table_id_idx ON metaschema_modules_public.billing_provider_module (prices_table_id); CREATE INDEX billing_provider_module_products_table_id_idx ON metaschema_modules_public.billing_provider_module (products_table_id); @@ -4487,6 +4507,7 @@ CREATE TABLE metaschema_modules_public.catalog_module ( resource_installations_table_id uuid NOT NULL DEFAULT uuid_nil(), apps_table_id uuid NOT NULL DEFAULT uuid_nil(), buckets_table_id uuid NOT NULL DEFAULT uuid_nil(), + bindings_table_id uuid NOT NULL DEFAULT uuid_nil(), sites_web_config_table_id uuid NOT NULL DEFAULT uuid_nil(), sites_error_pages_table_id uuid NOT NULL DEFAULT uuid_nil(), sites_app_links_table_id uuid NOT NULL DEFAULT uuid_nil(), @@ -4501,6 +4522,7 @@ CREATE TABLE metaschema_modules_public.catalog_module ( resource_installations_table_name text NOT NULL DEFAULT 'resource_installations', apps_table_name text NOT NULL DEFAULT 'apps', buckets_table_name text NOT NULL DEFAULT 'buckets', + bindings_table_name text NOT NULL DEFAULT 'bindings', sites_web_config_table_name text NOT NULL DEFAULT 'sites_web_config', sites_error_pages_table_name text NOT NULL DEFAULT 'sites_error_pages', sites_app_links_table_name text NOT NULL DEFAULT 'sites_app_links', @@ -4560,6 +4582,10 @@ CREATE TABLE metaschema_modules_public.catalog_module ( FOREIGN KEY(buckets_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT catalog_module_bindings_table_fkey + FOREIGN KEY(bindings_table_id) + REFERENCES metaschema_public.table (id) + ON DELETE CASCADE, CONSTRAINT catalog_module_sites_web_config_table_fkey FOREIGN KEY(sites_web_config_table_id) REFERENCES metaschema_public.table (id) @@ -4590,6 +4616,8 @@ CREATE INDEX catalog_module_apps_table_id_idx ON metaschema_modules_public.catal CREATE INDEX catalog_module_buckets_table_id_idx ON metaschema_modules_public.catalog_module (buckets_table_id); +CREATE INDEX catalog_module_bindings_table_id_idx ON metaschema_modules_public.catalog_module (bindings_table_id); + CREATE INDEX catalog_module_sites_web_config_table_id_idx ON metaschema_modules_public.catalog_module (sites_web_config_table_id); CREATE INDEX catalog_module_sites_error_pages_table_id_idx ON metaschema_modules_public.catalog_module (sites_error_pages_table_id); @@ -5163,4 +5191,66 @@ CREATE INDEX pages_module_private_schema_id_idx ON metaschema_modules_public.pag CREATE INDEX pages_module_public_schema_id_idx ON metaschema_modules_public.pages_module (public_schema_id); -CREATE INDEX pages_module_merkle_store_module_id_idx ON metaschema_modules_public.pages_module (merkle_store_module_id); \ No newline at end of file +CREATE INDEX pages_module_merkle_store_module_id_idx ON metaschema_modules_public.pages_module (merkle_store_module_id); + +CREATE TABLE metaschema_modules_public.user_settings_security_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + table_id uuid NOT NULL DEFAULT uuid_nil(), + owner_table_id uuid NOT NULL DEFAULT uuid_nil(), + table_name text NOT NULL DEFAULT 'user_settings_security', + api_name text DEFAULT NULL, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT table_fkey + FOREIGN KEY(table_id) + REFERENCES metaschema_public.table (id) + ON DELETE CASCADE, + CONSTRAINT owner_table_fkey + FOREIGN KEY(owner_table_id) + REFERENCES metaschema_public.table (id) + ON DELETE CASCADE +); + +CREATE UNIQUE INDEX user_settings_security_module_unique_per_db ON metaschema_modules_public.user_settings_security_module (database_id); + +CREATE INDEX user_settings_security_module_owner_table_id_idx ON metaschema_modules_public.user_settings_security_module (owner_table_id); + +CREATE INDEX user_settings_security_module_table_id_idx ON metaschema_modules_public.user_settings_security_module (table_id); + +CREATE INDEX user_settings_security_module_schema_id_idx ON metaschema_modules_public.user_settings_security_module (schema_id); + +CREATE TABLE metaschema_modules_public.scope_types_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_name text, + scope_types_table_id uuid NOT NULL DEFAULT uuid_nil(), + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT scope_types_table_fkey + FOREIGN KEY(scope_types_table_id) + REFERENCES metaschema_public.table (id) + ON DELETE CASCADE, + CONSTRAINT scope_types_module_unique + UNIQUE (database_id) +); + +CREATE INDEX scope_types_module_database_id_idx ON metaschema_modules_public.scope_types_module (database_id); + +CREATE INDEX scope_types_module_schema_id_idx ON metaschema_modules_public.scope_types_module (schema_id); + +CREATE INDEX scope_types_module_scope_types_table_id_idx ON metaschema_modules_public.scope_types_module (scope_types_table_id); \ No newline at end of file diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/schema.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/schema.sql index 4e919dcb5..f379f391f 100644 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/schema.sql +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/schema.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT pg_catalog.has_schema_privilege('metaschema_modules_public', 'usage'); +SELECT assert_schema('metaschema_modules_public'::regnamespace); ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/config_secrets_user_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/config_secrets_user_module/table.sql index 6ca9dcfd0..3a3592ff6 100644 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/config_secrets_user_module/table.sql +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/config_secrets_user_module/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_modules_public.config_secrets_user_module'); +SELECT assert_table('metaschema_modules_public.config_secrets_user_module'::regclass); ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/connected_accounts_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/connected_accounts_module/table.sql index c986b4547..43c142e01 100644 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/connected_accounts_module/table.sql +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/connected_accounts_module/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_modules_public.connected_accounts_module'); +SELECT assert_table('metaschema_modules_public.connected_accounts_module'::regclass); ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/crypto_addresses_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/crypto_addresses_module/table.sql index 7d0462db3..1fe048850 100644 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/crypto_addresses_module/table.sql +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/crypto_addresses_module/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_modules_public.crypto_addresses_module'); +SELECT assert_table('metaschema_modules_public.crypto_addresses_module'::regclass); ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/crypto_auth_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/crypto_auth_module/table.sql index 46e584b73..df4c2b9df 100644 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/crypto_auth_module/table.sql +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/crypto_auth_module/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_modules_public.crypto_auth_module'); +SELECT assert_table('metaschema_modules_public.crypto_auth_module'::regclass); ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/default_ids_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/default_ids_module/table.sql index 1333b7d37..389ad2ebc 100644 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/default_ids_module/table.sql +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/default_ids_module/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_modules_public.default_ids_module'); +SELECT assert_table('metaschema_modules_public.default_ids_module'::regclass); ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/denormalized_table_field/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/denormalized_table_field/table.sql index 2868c4e1f..7a5cd75a8 100644 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/denormalized_table_field/table.sql +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/denormalized_table_field/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_modules_public.denormalized_table_field'); +SELECT assert_table('metaschema_modules_public.denormalized_table_field'::regclass); ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/devices_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/devices_module/table.sql index c0c198e99..233650c32 100644 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/devices_module/table.sql +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/devices_module/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_modules_public.devices_module'); +SELECT assert_table('metaschema_modules_public.devices_module'::regclass); ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/emails_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/emails_module/table.sql index 5ea33e940..0e8ab149e 100644 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/emails_module/table.sql +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/emails_module/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_modules_public.emails_module'); +SELECT assert_table('metaschema_modules_public.emails_module'::regclass); ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/events_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/events_module/table.sql index fcc1bd224..c09d19eea 100644 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/events_module/table.sql +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/events_module/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_modules_public.events_module'); +SELECT assert_table('metaschema_modules_public.events_module'::regclass); ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/function_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/function_module/table.sql index 4af5faecb..7f3283485 100644 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/function_module/table.sql +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/function_module/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_modules_public.function_module'); +SELECT assert_table('metaschema_modules_public.function_module'::regclass); ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/graph_execution_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/graph_execution_module/table.sql index c50a56e23..51462e48b 100644 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/graph_execution_module/table.sql +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/graph_execution_module/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_modules_public.graph_execution_module'); +SELECT assert_table('metaschema_modules_public.graph_execution_module'::regclass); ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/identity_providers_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/identity_providers_module/table.sql index 35ba63265..5f73d6e23 100644 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/identity_providers_module/table.sql +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/identity_providers_module/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_modules_public.identity_providers_module'); +SELECT assert_table('metaschema_modules_public.identity_providers_module'::regclass); ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/invites_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/invites_module/table.sql index 6012c443a..69a2c1226 100644 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/invites_module/table.sql +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/invites_module/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_modules_public.invites_module'); +SELECT assert_table('metaschema_modules_public.invites_module'::regclass); ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/limits_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/limits_module/table.sql index fd92486f9..d55e28ce4 100644 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/limits_module/table.sql +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/limits_module/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_modules_public.limits_module'); +SELECT assert_table('metaschema_modules_public.limits_module'::regclass); ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/membership_types_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/membership_types_module/table.sql index 168025ddc..b5fbd9081 100644 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/membership_types_module/table.sql +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/membership_types_module/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_modules_public.membership_types_module'); +SELECT assert_table('metaschema_modules_public.membership_types_module'::regclass); ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/memberships_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/memberships_module/table.sql index 5518679b2..f41721288 100644 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/memberships_module/table.sql +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/memberships_module/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_modules_public.memberships_module'); +SELECT assert_table('metaschema_modules_public.memberships_module'::regclass); ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/notifications_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/notifications_module/table.sql index fe683e55b..480ce3917 100644 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/notifications_module/table.sql +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/notifications_module/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_modules_public.notifications_module'); +SELECT assert_table('metaschema_modules_public.notifications_module'::regclass); ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/permissions_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/permissions_module/table.sql index 45e2fa6d9..16d0f5f34 100644 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/permissions_module/table.sql +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/permissions_module/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_modules_public.permissions_module'); +SELECT assert_table('metaschema_modules_public.permissions_module'::regclass); ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/phone_numbers_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/phone_numbers_module/table.sql index f1e409513..76809c849 100644 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/phone_numbers_module/table.sql +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/phone_numbers_module/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_modules_public.phone_numbers_module'); +SELECT assert_table('metaschema_modules_public.phone_numbers_module'::regclass); ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/rate_limit_meters_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/rate_limit_meters_module/table.sql index 1053f4f62..0dde3c228 100644 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/rate_limit_meters_module/table.sql +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/rate_limit_meters_module/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_modules_public.rate_limit_meters_module'); +SELECT assert_table('metaschema_modules_public.rate_limit_meters_module'::regclass); ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/rate_limits_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/rate_limits_module/table.sql index 86e7fe89f..744f52492 100644 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/rate_limits_module/table.sql +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/rate_limits_module/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_modules_public.rate_limits_module'); +SELECT assert_table('metaschema_modules_public.rate_limits_module'::regclass); ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/realtime_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/realtime_module/table.sql index aca571c98..a5cfbad96 100644 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/realtime_module/table.sql +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/realtime_module/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_modules_public.realtime_module'); +SELECT assert_table('metaschema_modules_public.realtime_module'::regclass); ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/rls_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/rls_module/table.sql index 122261840..05b5fcc68 100644 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/rls_module/table.sql +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/rls_module/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_modules_public.rls_module'); +SELECT assert_table('metaschema_modules_public.rls_module'::regclass); ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/scope_types_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/scope_types_module/table.sql new file mode 100644 index 000000000..a4589cd4d --- /dev/null +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/scope_types_module/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/scope_types_module/table on pg + +BEGIN; + +SELECT assert_table('metaschema_modules_public.scope_types_module'::regclass); + +ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/sessions_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/sessions_module/table.sql index 199463cae..aa6f0b56b 100644 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/sessions_module/table.sql +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/sessions_module/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_modules_public.sessions_module'); +SELECT assert_table('metaschema_modules_public.sessions_module'::regclass); ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/storage_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/storage_module/table.sql index 47099c408..b05fe1931 100644 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/storage_module/table.sql +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/storage_module/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_modules_public.storage_module'); +SELECT assert_table('metaschema_modules_public.storage_module'::regclass); ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/user_auth_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/user_auth_module/table.sql index 7ac2d45bb..ee3732c9a 100644 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/user_auth_module/table.sql +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/user_auth_module/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_modules_public.user_auth_module'); +SELECT assert_table('metaschema_modules_public.user_auth_module'::regclass); ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/user_credentials_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/user_credentials_module/table.sql index 940681c32..6fea0bd76 100644 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/user_credentials_module/table.sql +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/user_credentials_module/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table('metaschema_modules_public.user_credentials_module'); +SELECT assert_table('metaschema_modules_public.user_credentials_module'::regclass); ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/user_settings_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/user_settings_module/table.sql index 2ed6c6a98..2e5891017 100644 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/user_settings_module/table.sql +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/user_settings_module/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_modules_public.user_settings_module'); +SELECT assert_table('metaschema_modules_public.user_settings_module'::regclass); ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/user_settings_security_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/user_settings_security_module/table.sql new file mode 100644 index 000000000..cd5c88197 --- /dev/null +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/user_settings_security_module/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/user_settings_security_module/table on pg + +BEGIN; + +SELECT assert_table('metaschema_modules_public.user_settings_security_module'::regclass); + +ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/user_state_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/user_state_module/table.sql index 36655782d..542b66519 100644 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/user_state_module/table.sql +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/user_state_module/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_modules_public.user_state_module'); +SELECT assert_table('metaschema_modules_public.user_state_module'::regclass); ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/users_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/users_module/table.sql index 2b0557cc1..2c0c50980 100644 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/users_module/table.sql +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/users_module/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_modules_public.users_module'); +SELECT assert_table('metaschema_modules_public.users_module'::regclass); ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/webauthn_auth_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/webauthn_auth_module/table.sql index 8fc84c14d..b75703866 100644 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/webauthn_auth_module/table.sql +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/webauthn_auth_module/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_modules_public.webauthn_auth_module'); +SELECT assert_table('metaschema_modules_public.webauthn_auth_module'::regclass); ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/webauthn_credentials_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/webauthn_credentials_module/table.sql index a8859bf56..e7fad0ac7 100644 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/webauthn_credentials_module/table.sql +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/webauthn_credentials_module/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_modules_public.webauthn_credentials_module'); +SELECT assert_table('metaschema_modules_public.webauthn_credentials_module'::regclass); ROLLBACK; diff --git a/packages/metaschema-schema/Makefile b/packages/metaschema-schema/Makefile index 18e4917e7..02f89d4f8 100644 --- a/packages/metaschema-schema/Makefile +++ b/packages/metaschema-schema/Makefile @@ -1,5 +1,5 @@ EXTENSION = metaschema-schema -DATA = sql/metaschema-schema--0.36.0.sql +DATA = sql/metaschema-schema--0.39.0.sql PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) diff --git a/packages/metaschema-schema/metaschema-schema.control b/packages/metaschema-schema/metaschema-schema.control index 246930e28..40a8bbf8d 100644 --- a/packages/metaschema-schema/metaschema-schema.control +++ b/packages/metaschema-schema/metaschema-schema.control @@ -1,6 +1,6 @@ # metaschema-schema extension comment = 'metaschema-schema extension' -default_version = '0.36.0' +default_version = '0.39.0' module_pathname = '$libdir/metaschema-schema' requires = 'citext,hstore,pgpm-inflection,pgpm-database-jobs,pgpm-types,pgcrypto,plpgsql,postgis,uuid-ossp,pgpm-verify' relocatable = false diff --git a/packages/metaschema-schema/package.json b/packages/metaschema-schema/package.json index e2fd0851d..bb4f86c84 100644 --- a/packages/metaschema-schema/package.json +++ b/packages/metaschema-schema/package.json @@ -1,6 +1,6 @@ { "name": "@pgpm/metaschema-schema", - "version": "0.36.0", + "version": "0.39.0", "description": "Database metadata utilities and introspection functions", "author": "Dan Lynch ", "contributors": [ diff --git a/packages/metaschema-schema/revert/schemas/metaschema_private/procedures/is_valid_step_up.sql b/packages/metaschema-schema/revert/schemas/metaschema_private/procedures/is_valid_step_up.sql index 95ed5e4be..9d342edaa 100644 --- a/packages/metaschema-schema/revert/schemas/metaschema_private/procedures/is_valid_step_up.sql +++ b/packages/metaschema-schema/revert/schemas/metaschema_private/procedures/is_valid_step_up.sql @@ -2,8 +2,7 @@ BEGIN; -DROP FUNCTION metaschema_private.is_valid_step_up; - -DROP FUNCTION metaschema_private.is_valid_step_up_conditions; +DROP FUNCTION metaschema_private.is_valid_step_up(jsonb); +DROP FUNCTION metaschema_private.is_valid_step_up_conditions(jsonb); COMMIT; diff --git a/packages/metaschema-schema/revert/schemas/metaschema_public/tables/database/indexes/databases_database_unique_name_idx.sql b/packages/metaschema-schema/revert/schemas/metaschema_public/tables/database/indexes/databases_database_unique_name_idx.sql index 3410c5a89..a0bf22d89 100644 --- a/packages/metaschema-schema/revert/schemas/metaschema_public/tables/database/indexes/databases_database_unique_name_idx.sql +++ b/packages/metaschema-schema/revert/schemas/metaschema_public/tables/database/indexes/databases_database_unique_name_idx.sql @@ -1,6 +1,8 @@ +-- Revert schemas/metaschema_public/tables/database/indexes/databases_database_unique_name_idx from pg BEGIN; DROP INDEX metaschema_public.databases_database_unique_name_idx; +DROP FUNCTION metaschema_private.database_name_hash(text); COMMIT; diff --git a/packages/metaschema-schema/revert/schemas/metaschema_public/tables/schema/triggers/enforce_api_exposure_ratchet.sql b/packages/metaschema-schema/revert/schemas/metaschema_public/tables/schema/triggers/enforce_api_exposure_ratchet.sql index 4efcd17f3..0f5d9f51e 100644 --- a/packages/metaschema-schema/revert/schemas/metaschema_public/tables/schema/triggers/enforce_api_exposure_ratchet.sql +++ b/packages/metaschema-schema/revert/schemas/metaschema_public/tables/schema/triggers/enforce_api_exposure_ratchet.sql @@ -2,7 +2,7 @@ BEGIN; -DROP TRIGGER IF EXISTS _000003_enforce_api_exposure_ratchet ON metaschema_public.schema; -DROP FUNCTION IF EXISTS metaschema_public.tg_enforce_api_exposure_ratchet(); +DROP TRIGGER _000003_enforce_api_exposure_ratchet ON metaschema_public.schema; +DROP FUNCTION metaschema_public.tg_enforce_api_exposure_ratchet(); COMMIT; diff --git a/packages/metaschema-schema/revert/schemas/metaschema_public/tables/table/indexes/databases_table_unique_name_idx.sql b/packages/metaschema-schema/revert/schemas/metaschema_public/tables/table/indexes/databases_table_unique_name_idx.sql index fad5449f9..004003439 100644 --- a/packages/metaschema-schema/revert/schemas/metaschema_public/tables/table/indexes/databases_table_unique_name_idx.sql +++ b/packages/metaschema-schema/revert/schemas/metaschema_public/tables/table/indexes/databases_table_unique_name_idx.sql @@ -1,6 +1,8 @@ +-- Revert schemas/metaschema_public/tables/table/indexes/databases_table_unique_name_idx from pg BEGIN; DROP INDEX metaschema_public.databases_table_unique_name_idx; +DROP FUNCTION metaschema_private.table_name_hash(text); COMMIT; diff --git a/packages/metaschema-schema/sql/metaschema-schema--0.36.0.bundle.tar.gz b/packages/metaschema-schema/sql/metaschema-schema--0.36.0.bundle.tar.gz deleted file mode 100644 index 095bf9459..000000000 Binary files a/packages/metaschema-schema/sql/metaschema-schema--0.36.0.bundle.tar.gz and /dev/null differ diff --git a/packages/metaschema-schema/sql/metaschema-schema--0.39.0.bundle.tar.gz b/packages/metaschema-schema/sql/metaschema-schema--0.39.0.bundle.tar.gz new file mode 100644 index 000000000..a84a63f75 Binary files /dev/null and b/packages/metaschema-schema/sql/metaschema-schema--0.39.0.bundle.tar.gz differ diff --git a/packages/metaschema-schema/sql/metaschema-schema--0.36.0.sql b/packages/metaschema-schema/sql/metaschema-schema--0.39.0.sql similarity index 100% rename from packages/metaschema-schema/sql/metaschema-schema--0.36.0.sql rename to packages/metaschema-schema/sql/metaschema-schema--0.39.0.sql diff --git a/packages/metaschema-schema/verify/schemas/metaschema_private/procedures/is_valid_step_up.sql b/packages/metaschema-schema/verify/schemas/metaschema_private/procedures/is_valid_step_up.sql index cf56d543b..1d8d2b5fa 100644 --- a/packages/metaschema-schema/verify/schemas/metaschema_private/procedures/is_valid_step_up.sql +++ b/packages/metaschema-schema/verify/schemas/metaschema_private/procedures/is_valid_step_up.sql @@ -2,8 +2,8 @@ BEGIN; -SELECT verify_function ('metaschema_private.is_valid_step_up'); +SELECT assert_function('metaschema_private.is_valid_step_up(jsonb)'::regprocedure); -SELECT verify_function ('metaschema_private.is_valid_step_up_conditions'); +SELECT assert_function('metaschema_private.is_valid_step_up_conditions(jsonb)'::regprocedure); ROLLBACK; diff --git a/packages/metaschema-schema/verify/schemas/metaschema_private/schema.sql b/packages/metaschema-schema/verify/schemas/metaschema_private/schema.sql index 41c185622..505a4abc3 100644 --- a/packages/metaschema-schema/verify/schemas/metaschema_private/schema.sql +++ b/packages/metaschema-schema/verify/schemas/metaschema_private/schema.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_schema ('metaschema_private'); +SELECT assert_schema('metaschema_private'::regnamespace); ROLLBACK; diff --git a/packages/metaschema-schema/verify/schemas/metaschema_public/schema.sql b/packages/metaschema-schema/verify/schemas/metaschema_public/schema.sql index 7600f8c68..7b59070dc 100644 --- a/packages/metaschema-schema/verify/schemas/metaschema_public/schema.sql +++ b/packages/metaschema-schema/verify/schemas/metaschema_public/schema.sql @@ -1,6 +1,6 @@ BEGIN; -SELECT verify_schema ('metaschema_public'); +SELECT assert_schema('metaschema_public'::regnamespace); ROLLBACK; diff --git a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/check_constraint/table.sql b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/check_constraint/table.sql index 4bab07c37..f8ad8650a 100644 --- a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/check_constraint/table.sql +++ b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/check_constraint/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_public.check_constraint'); +SELECT assert_table('metaschema_public.check_constraint'::regclass); ROLLBACK; diff --git a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/composite_type/table.sql b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/composite_type/table.sql index de512a0cb..e73aa5ce6 100644 --- a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/composite_type/table.sql +++ b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/composite_type/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_public.composite_type'); +SELECT assert_table('metaschema_public.composite_type'::regclass); ROLLBACK; diff --git a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/database/indexes/databases_database_unique_name_idx.sql b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/database/indexes/databases_database_unique_name_idx.sql index d5e64f0b9..844d5329f 100644 --- a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/database/indexes/databases_database_unique_name_idx.sql +++ b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/database/indexes/databases_database_unique_name_idx.sql @@ -1,6 +1,6 @@ BEGIN; -SELECT verify_index ('metaschema_public.database', 'databases_database_unique_name_idx'); +SELECT assert_index('metaschema_public.databases_database_unique_name_idx'::regclass, 'metaschema_public.database'::regclass, true); ROLLBACK; diff --git a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/database/table.sql b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/database/table.sql index 2644ae259..4c4ee2c2d 100644 --- a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/database/table.sql +++ b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/database/table.sql @@ -1,6 +1,6 @@ BEGIN; -SELECT verify_table ('metaschema_public.database'); +SELECT assert_table('metaschema_public.database'::regclass); ROLLBACK; diff --git a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/domain_type/table.sql b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/domain_type/table.sql index 54223a014..5e0421e5a 100644 --- a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/domain_type/table.sql +++ b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/domain_type/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_public.domain_type'); +SELECT assert_table('metaschema_public.domain_type'::regclass); ROLLBACK; diff --git a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/enum/table.sql b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/enum/table.sql index 34ad6c30f..0629382e1 100644 --- a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/enum/table.sql +++ b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/enum/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_public.enum'); +SELECT assert_table('metaschema_public.enum'::regclass); ROLLBACK; diff --git a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/exclusion_constraint/table.sql b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/exclusion_constraint/table.sql index 266e4a9e7..ef707e2b7 100644 --- a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/exclusion_constraint/table.sql +++ b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/exclusion_constraint/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_public.exclusion_constraint'); +SELECT assert_table('metaschema_public.exclusion_constraint'::regclass); ROLLBACK; diff --git a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/field/indexes/databases_field_uniq_names_idx.sql b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/field/indexes/databases_field_uniq_names_idx.sql index 6541e5229..8d13a6160 100644 --- a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/field/indexes/databases_field_uniq_names_idx.sql +++ b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/field/indexes/databases_field_uniq_names_idx.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_index ('metaschema_public.field', 'databases_field_uniq_names_idx'); +SELECT assert_index('metaschema_public.databases_field_uniq_names_idx'::regclass, 'metaschema_public.field'::regclass, true); ROLLBACK; diff --git a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/field/table.sql b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/field/table.sql index 20da5a3a9..40e68099f 100644 --- a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/field/table.sql +++ b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/field/table.sql @@ -1,6 +1,6 @@ BEGIN; -SELECT verify_table ('metaschema_public.field'); +SELECT assert_table('metaschema_public.field'::regclass); ROLLBACK; diff --git a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/foreign_key_constraint/table.sql b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/foreign_key_constraint/table.sql index 52ebe68bb..361d3a056 100644 --- a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/foreign_key_constraint/table.sql +++ b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/foreign_key_constraint/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_public.foreign_key_constraint'); +SELECT assert_table('metaschema_public.foreign_key_constraint'::regclass); ROLLBACK; diff --git a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/full_text_search/table.sql b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/full_text_search/table.sql index 23d4a04f2..2c5772609 100644 --- a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/full_text_search/table.sql +++ b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/full_text_search/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_public.full_text_search'); +SELECT assert_table('metaschema_public.full_text_search'::regclass); ROLLBACK; diff --git a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/function/table.sql b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/function/table.sql index fc8976f29..ae36d2213 100644 --- a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/function/table.sql +++ b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/function/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_public.function'); +SELECT assert_table('metaschema_public.function'::regclass); ROLLBACK; diff --git a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/index/table.sql b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/index/table.sql index 6529f6124..5bcaa7661 100644 --- a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/index/table.sql +++ b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/index/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_public.index'); +SELECT assert_table('metaschema_public.index'::regclass); ROLLBACK; diff --git a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/partition/table.sql b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/partition/table.sql index a37df6849..011799a7d 100644 --- a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/partition/table.sql +++ b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/partition/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_public.partition'); +SELECT assert_table('metaschema_public.partition'::regclass); ROLLBACK; diff --git a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/policy/table.sql b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/policy/table.sql index 72ae6169a..a5148c8b4 100644 --- a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/policy/table.sql +++ b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/policy/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_public.policy'); +SELECT assert_table('metaschema_public.policy'::regclass); ROLLBACK; diff --git a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/primary_key_constraint/table.sql b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/primary_key_constraint/table.sql index 22a74fc5e..8cad5c308 100644 --- a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/primary_key_constraint/table.sql +++ b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/primary_key_constraint/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_public.primary_key_constraint'); +SELECT assert_table('metaschema_public.primary_key_constraint'::regclass); ROLLBACK; diff --git a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/schema/table.sql b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/schema/table.sql index 7d37ed9c6..c11b139a4 100644 --- a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/schema/table.sql +++ b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/schema/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_public.schema'); +SELECT assert_table('metaschema_public.schema'::regclass); ROLLBACK; diff --git a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/schema/triggers/enforce_api_exposure_ratchet.sql b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/schema/triggers/enforce_api_exposure_ratchet.sql index 3799dc231..46c887c1d 100644 --- a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/schema/triggers/enforce_api_exposure_ratchet.sql +++ b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/schema/triggers/enforce_api_exposure_ratchet.sql @@ -2,9 +2,6 @@ BEGIN; -SELECT has_function_privilege( - 'metaschema_public.tg_enforce_api_exposure_ratchet()', - 'execute' -); +SELECT assert_function('metaschema_public.tg_enforce_api_exposure_ratchet()'::regprocedure); ROLLBACK; diff --git a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/schema_grant/table.sql b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/schema_grant/table.sql index a630e80b7..a14020b9d 100644 --- a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/schema_grant/table.sql +++ b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/schema_grant/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_public.schema_grant'); +SELECT assert_table('metaschema_public.schema_grant'::regclass); ROLLBACK; diff --git a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/spatial_relation/table.sql b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/spatial_relation/table.sql index 68c821b3d..a08522ab1 100644 --- a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/spatial_relation/table.sql +++ b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/spatial_relation/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_public.spatial_relation'); +SELECT assert_table('metaschema_public.spatial_relation'::regclass); ROLLBACK; diff --git a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/table/indexes/databases_table_unique_name_idx.sql b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/table/indexes/databases_table_unique_name_idx.sql index 9e71e8502..1ecbf5998 100644 --- a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/table/indexes/databases_table_unique_name_idx.sql +++ b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/table/indexes/databases_table_unique_name_idx.sql @@ -1,6 +1,6 @@ BEGIN; -SELECT verify_index ('metaschema_public.table', 'databases_table_unique_name_idx'); +SELECT assert_index('metaschema_public.databases_table_unique_name_idx'::regclass, 'metaschema_public.table'::regclass, true); ROLLBACK; diff --git a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/table/table.sql b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/table/table.sql index cbfc4432a..d686e6ba3 100644 --- a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/table/table.sql +++ b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/table/table.sql @@ -1,6 +1,6 @@ BEGIN; -SELECT verify_table ('metaschema_public.table'); +SELECT assert_table('metaschema_public.table'::regclass); ROLLBACK; diff --git a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/table_grant/table.sql b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/table_grant/table.sql index 77ebaac86..1e15a098a 100644 --- a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/table_grant/table.sql +++ b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/table_grant/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_public.table_grant'); +SELECT assert_table('metaschema_public.table_grant'::regclass); ROLLBACK; diff --git a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/trigger/table.sql b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/trigger/table.sql index 5ab33b986..3417b0827 100644 --- a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/trigger/table.sql +++ b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/trigger/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_public.trigger'); +SELECT assert_table('metaschema_public.trigger'::regclass); ROLLBACK; diff --git a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/trigger_function/table.sql b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/trigger_function/table.sql index bb29f9073..8ffee0f2a 100644 --- a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/trigger_function/table.sql +++ b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/trigger_function/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_public.trigger_function'); +SELECT assert_table('metaschema_public.trigger_function'::regclass); ROLLBACK; diff --git a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/unique_constraint/table.sql b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/unique_constraint/table.sql index f4e6b519c..a93122e99 100644 --- a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/unique_constraint/table.sql +++ b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/unique_constraint/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('metaschema_public.unique_constraint'); +SELECT assert_table('metaschema_public.unique_constraint'::regclass); ROLLBACK; diff --git a/packages/object-store/Makefile b/packages/object-store/Makefile index 1e2f7b9a7..9e90b0d16 100644 --- a/packages/object-store/Makefile +++ b/packages/object-store/Makefile @@ -1,5 +1,5 @@ EXTENSION = object-store -DATA = sql/object-store--0.36.0.sql +DATA = sql/object-store--0.39.0.sql PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) diff --git a/packages/object-store/__tests__/objects/insert-nodes-at-paths.test.ts b/packages/object-store/__tests__/objects/insert-nodes-at-paths.test.ts new file mode 100644 index 000000000..13d9bfca8 --- /dev/null +++ b/packages/object-store/__tests__/objects/insert-nodes-at-paths.test.ts @@ -0,0 +1,278 @@ +jest.setTimeout(60000); + +import { getConnections, PgTestClient } from 'pgsql-test'; + +let pg: PgTestClient; +let teardown: () => Promise; + +// One scope per tree: an empty root is a content hash like any other node, so +// two empty trees in one scope would collide on the primary key. +const eager_scope = '2b8b6d4c-8b8c-4a0a-9b9d-1d0a3f6a1c21'; +const batched_scope = '9a1c0f52-3f2e-4a77-8a2c-6f7c1b0d5e34'; + +beforeAll(async () => { + ({ pg, teardown } = await getConnections()); +}); + +afterAll(async () => { + try { + await teardown(); + } catch (e) { + // ignore + } +}); + +beforeEach(async () => { + await pg.beforeEach(); +}); + +afterEach(async () => { + await pg.afterEach(); +}); + +interface Write { + path: string[]; + data: any; +} + +const newRoot = async (scope_id: string) => { + const [row] = await pg.any( + `INSERT INTO object_store_public.object (scope_id) VALUES ($1) RETURNING id`, + [scope_id] + ); + return row.id as string; +}; + +const insertNode = async (scope_id: string, rootId: string, { path, data }: Write) => { + const [row] = await pg.any( + `SELECT object_store_public.insert_node_at_path( + s_id := $1::uuid, + root := $2::uuid, + path := $3::text[], + data := $4::jsonb, + kids := '{}'::uuid[], + ktree := '{}'::text[] + ) AS new_root`, + [scope_id, rootId, path, data] + ); + return row.new_root as string; +}; + +const insertNodes = async (scope_id: string, rootId: string, writes: Write[]) => { + const [row] = await pg.any( + `SELECT object_store_public.insert_nodes_at_paths( + s_id := $1::uuid, + root := $2::uuid, + paths := $3::jsonb, + datas := $4::jsonb[], + kids_list := $5::jsonb, + ktree_list := $6::jsonb + ) AS new_root`, + [ + scope_id, + rootId, + JSON.stringify(writes.map((w) => w.path)), + writes.map((w) => JSON.stringify(w.data)), + JSON.stringify(writes.map((): string[] => [])), + JSON.stringify(writes.map((): string[] => [])) + ] + ); + return row.new_root as string; +}; + +// The batch with kids/ktree left to the caller: `null` means "no children +// given", an empty array means "given, and empty", and the two hash differently. +const insertNodesRaw = async ( + scope_id: string, + rootId: string, + paths: string[][], + datas: any[], + kids_list: (string[] | null)[] | null, + ktree_list: (string[] | null)[] | null +) => { + const [row] = await pg.any( + `SELECT object_store_public.insert_nodes_at_paths( + s_id := $1::uuid, + root := $2::uuid, + paths := $3::jsonb, + datas := $4::jsonb[], + kids_list := $5::jsonb, + ktree_list := $6::jsonb + ) AS new_root`, + [ + scope_id, + rootId, + JSON.stringify(paths), + datas.map((d) => JSON.stringify(d)), + kids_list === null ? null : JSON.stringify(kids_list), + ktree_list === null ? null : JSON.stringify(ktree_list) + ] + ); + return row.new_root as string; +}; + +const eagerly = async (scope_id: string, rootId: string, writes: Write[]) => { + let current = rootId; + for (const write of writes) { + current = await insertNode(scope_id, current, write); + } + return current; +}; + +const getNode = async (rootId: string, path: string[], scope_id = batched_scope) => { + const [row] = await pg.any( + `SELECT * FROM object_store_public.get_node_at_path( + s_id := $1::uuid, id := $2::uuid, path := $3::text[] + )`, + [scope_id, rootId, path] + ); + return row; +}; + +// The same writes applied one at a time and applied as one batch must produce +// the identical root: node ids are pure content hashes, with no timestamp, +// sequence or insertion order in them. +const expectSameRoot = async (writes: Write[], seed: Write[] = []) => { + const eagerRoot = await eagerly( + eager_scope, + await eagerly(eager_scope, await newRoot(eager_scope), seed), + writes + ); + const batchedRoot = await insertNodes( + batched_scope, + await eagerly(batched_scope, await newRoot(batched_scope), seed), + writes + ); + expect(batchedRoot).toEqual(eagerRoot); + return batchedRoot; +}; + +describe('insert_nodes_at_paths equivalence with the eager path', () => { + it('single write', async () => { + await expectSameRoot([{ path: ['a', 'b', 'c'], data: { v: 1 } }]); + }); + + it('write at the root itself', async () => { + await expectSameRoot([{ path: [], data: { v: 'root' } }]); + }); + + it('deeply nested path', async () => { + await expectSameRoot([ + { path: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], data: { deep: true } } + ]); + }); + + it('duplicate paths in one batch: last write wins', async () => { + const root = await expectSameRoot([ + { path: ['x', 'y'], data: { v: 1 } }, + { path: ['x', 'y'], data: { v: 2 } }, + { path: ['x', 'y'], data: { v: 3 } } + ]); + expect((await getNode(root, ['x', 'y'])).data).toEqual({ v: 3 }); + }); + + it('a write whose path is a prefix of another write in the same batch', async () => { + const root = await expectSameRoot([ + { path: ['dir'], data: { on: 'dir' } }, + { path: ['dir', 'leaf'], data: { on: 'leaf' } } + ]); + expect((await getNode(root, ['dir'])).data).toEqual({ on: 'dir' }); + expect((await getNode(root, ['dir', 'leaf'])).data).toEqual({ + on: 'leaf' + }); + }); + + it('merging into a directory that already has children keeps them', async () => { + const seed: Write[] = [ + { path: ['dir', 'old1'], data: { o: 1 } }, + { path: ['dir', 'old2'], data: { o: 2 } }, + { path: ['other', 'keep'], data: { k: 1 } } + ]; + const root = await expectSameRoot( + [ + { path: ['dir', 'new1'], data: { n: 1 } }, + { path: ['dir', 'new2'], data: { n: 2 } } + ], + seed + ); + + const dir = await getNode(root, ['dir']); + expect(dir.ktree.sort()).toEqual(['new1', 'new2', 'old1', 'old2']); + expect((await getNode(root, ['dir', 'old1'])).data).toEqual({ o: 1 }); + expect((await getNode(root, ['other', 'keep'])).data).toEqual({ k: 1 }); + }); + + it('overwriting an existing leaf', async () => { + const root = await expectSameRoot([{ path: ['dir', 'leaf'], data: { v: 2 } }], [ + { path: ['dir', 'leaf'], data: { v: 1 } }, + { path: ['dir', 'sibling'], data: { v: 9 } } + ]); + expect((await getNode(root, ['dir', 'leaf'])).data).toEqual({ v: 2 }); + expect((await getNode(root, ['dir', 'sibling'])).data).toEqual({ v: 9 }); + }); + + it('200 changes / 800 paths produce the identical root', async () => { + const writes: Write[] = []; + for (let i = 1; i <= 200; i++) { + for (const part of ['deploy', 'revert', 'verify', 'meta']) { + writes.push({ + path: ['sql', 'deploy', `change_${i}`, part], + data: { i, part } + }); + } + } + await expectSameRoot(writes); + }); + + it('omitted kids/ktree are not the same node as empty ones', async () => { + const path = [['a', 'leaf']]; + const data = [{ v: 1 }]; + + const omitted = await insertNodesRaw( + batched_scope, + await newRoot(batched_scope), + path, + data, + null, + null + ); + const empty = await insertNodesRaw( + eager_scope, + await newRoot(eager_scope), + path, + data, + [[]], + [[]] + ); + + expect(omitted).not.toEqual(empty); + expect((await getNode(omitted, ['a', 'leaf'])).ktree).toBeNull(); + expect((await getNode(empty, ['a', 'leaf'], eager_scope)).ktree).toEqual([]); + }); + + it('explicit kids/ktree on a write are stored as given', async () => { + const seeded = await insertNodes(batched_scope, await newRoot(batched_scope), [ + { path: ['target'], data: { t: 1 } } + ]); + const target = (await getNode(seeded, ['target'])).id as string; + + const root = await insertNodesRaw( + batched_scope, + seeded, + [['dir', 'linker']], + [{ v: 'links' }], + [[target]], + [['alias']] + ); + + const linker = await getNode(root, ['dir', 'linker']); + expect(linker.ktree).toEqual(['alias']); + expect(linker.kids).toEqual([target]); + expect((await getNode(root, ['dir', 'linker', 'alias'])).data).toEqual({ t: 1 }); + }); + + it('empty input returns the root unchanged', async () => { + const root = await newRoot(batched_scope); + expect(await insertNodes(batched_scope, root, [])).toEqual(root); + }); +}); diff --git a/packages/object-store/deploy/schemas/object_store_private/procedures/node_hash_uuid.sql b/packages/object-store/deploy/schemas/object_store_private/procedures/node_hash_uuid.sql new file mode 100644 index 000000000..cc3f9a88c --- /dev/null +++ b/packages/object-store/deploy/schemas/object_store_private/procedures/node_hash_uuid.sql @@ -0,0 +1,23 @@ +-- Deploy schemas/object_store_private/procedures/node_hash_uuid to pg + +-- requires: schemas/object_store_private/schema +-- requires: schemas/object_store_public/tables/object/table +-- requires: schemas/object_store_private/procedures/object_hash_uuid + +BEGIN; + +-- The content hash of a node from its hashed fields alone, without a row to +-- read it off. A set-based writer needs the ids of the rows it is about to +-- insert in order to name them in their parents' child maps, which the +-- BEFORE INSERT trigger's RETURNING id can only give one row at a time. The +-- record is built by name so this stays correct if the table gains a column. +CREATE FUNCTION object_store_private.node_hash_uuid (data jsonb, kids uuid[], ktree text[]) + RETURNS uuid + AS $$ + SELECT + object_store_public.object_hash_uuid (jsonb_populate_record(NULL::object_store_public.object, jsonb_build_object('data', node_hash_uuid.data, 'kids', to_jsonb(node_hash_uuid.kids), 'ktree', to_jsonb(node_hash_uuid.ktree)))); +$$ +LANGUAGE sql +IMMUTABLE; + +COMMIT; diff --git a/packages/object-store/deploy/schemas/object_store_public/procedures/insert_node_at_path.sql b/packages/object-store/deploy/schemas/object_store_public/procedures/insert_node_at_path.sql index 86466c32d..9d155b64b 100644 --- a/packages/object-store/deploy/schemas/object_store_public/procedures/insert_node_at_path.sql +++ b/packages/object-store/deploy/schemas/object_store_public/procedures/insert_node_at_path.sql @@ -7,152 +7,18 @@ BEGIN; +-- One-element wrapper over object_store_public.insert_nodes_at_paths, which is +-- created later in the plan and resolved at call time. Identical signature, +-- identical result: batching is no longer a mode, only how much a caller +-- passes in. CREATE FUNCTION object_store_public.insert_node_at_path (s_id uuid, root uuid, path text[], data jsonb, kids uuid[], ktree text[]) RETURNS uuid AS $$ -DECLARE - _newnode_id uuid; - _newparent_id uuid; - _parent object_store_public.object; - _orig_name text; - _repl uuid; - _uuid_to_return uuid; - _parent_existed bool; - - vkids uuid[]; - vktree text[]; - - i int; - _path_len int; - _depth int; - _pos int; - _cur_id uuid; - _cur_obj object_store_public.object; - - -- Cached nodes along the path for O(D) bottom-up rebuild. - -- Index 1 = root (depth 0), index k+1 = node at path[1..k] (depth k). - _cached object_store_public.object[]; - - children_hash jsonb; - BEGIN - - _path_len = coalesce(array_length(path, 1), 0); - - -- STEP 1: Walk down the path from root, caching each node. - -- Replaces O(D) call to get_node_at_path AND pre-populates the - -- cache so the bottom-up rebuild avoids re-walking from root - -- at each level (eliminates the O(D²) repeated walks). - - SELECT * FROM object_store_public.object o - WHERE o.id = insert_node_at_path.root - AND o.scope_id = s_id - INTO _cur_obj; - - _cached[1] = _cur_obj; - _depth = 0; - - IF (_cur_obj.id IS NOT NULL AND _path_len > 0) THEN - FOR i IN 1.._path_len LOOP - _pos = object_store_utils.array_index_of(_cur_obj.ktree, path[i]); - IF (_pos > 0) THEN - _cur_id = _cur_obj.kids[_pos]; - SELECT * FROM object_store_public.object o - WHERE o.id = _cur_id - AND o.scope_id = s_id - INTO _cur_obj; - _cached[i + 1] = _cur_obj; - _depth = i; - ELSE - EXIT; - END IF; - END LOOP; - END IF; - - -- STEP 2: Insert the new node - INSERT INTO object_store_public.object (scope_id, data, kids, ktree) - VALUES (s_id, insert_node_at_path.data, insert_node_at_path.kids, insert_node_at_path.ktree) - ON CONFLICT (id, scope_id) - DO UPDATE -- DO NOT USE NOTHING! won't return an ID - SET scope_id = EXCLUDED.scope_id - RETURNING - id - INTO _newnode_id; - - IF (_newnode_id IS NULL) THEN - RAISE EXCEPTION '_newnode_id failed'; - END IF; - - _orig_name = path[_path_len]; - _repl = _newnode_id; - _uuid_to_return = _newnode_id; - - -- STEP 3: Walk back up, creating new parent nodes with updated children. - -- Each parent is read from _cached instead of re-walking from root. - FOR i IN REVERSE _path_len..1 LOOP - - -- Parent is at depth i-1, stored in _cached[i] - IF (i - 1 = 0) THEN - _parent = _cached[1]; - _parent_existed = ((_parent).id IS NOT NULL); - ELSIF (i - 1 <= _depth) THEN - _parent = _cached[i]; - _parent_existed = TRUE; - ELSE - _parent_existed = FALSE; - END IF; - - IF (_parent_existed) THEN - - -- Update parent's children: jsonb_set replaces if key exists, adds if not - children_hash = object_store_utils.zip_arrays( - _parent.ktree, - _parent.kids - ); - - children_hash = jsonb_set(children_hash, ARRAY[_orig_name]::text[], to_jsonb(_repl)); - - SELECT h.ktree, h.kids FROM object_store_utils.unzip_obj_to_ktree_and_kids( - children_hash - ) h INTO vktree, vkids; - - INSERT INTO object_store_public.object (scope_id, data, kids, ktree) - VALUES (s_id, _parent.data, vkids, vktree) - ON CONFLICT (id, scope_id) - DO UPDATE -- DO NOT USE NOTHING! won't return an ID - SET scope_id = EXCLUDED.scope_id - RETURNING - id - INTO _newparent_id; - - IF (_newparent_id IS NULL) THEN - RAISE EXCEPTION 'parent insert failed at depth %', i - 1; - END IF; - - ELSE - - -- Parent doesn't exist: create new node with single child - INSERT INTO object_store_public.object (scope_id, data, kids, ktree) - VALUES (s_id, NULL, ARRAY[_repl]::uuid[], ARRAY[_orig_name]::text[]) - ON CONFLICT (id, scope_id) - DO UPDATE -- DO NOT USE NOTHING! won't return an ID - SET scope_id = EXCLUDED.scope_id - RETURNING - id - INTO _newparent_id; - - END IF; - - -- Move up: this parent becomes the child for the next level - _orig_name = path[i - 1]; - _repl = _newparent_id; - _uuid_to_return = _newparent_id; - - END LOOP; - - RETURN _uuid_to_return; + RETURN object_store_public.insert_nodes_at_paths (s_id := insert_node_at_path.s_id, root := insert_node_at_path.root, paths := jsonb_build_array(coalesce(to_jsonb(insert_node_at_path.path), '[]'::jsonb)), datas := ARRAY[insert_node_at_path.data]::jsonb[], kids_list := jsonb_build_array(to_jsonb(insert_node_at_path.kids)), ktree_list := jsonb_build_array(to_jsonb(insert_node_at_path.ktree))); END; $$ LANGUAGE plpgsql VOLATILE; + COMMIT; diff --git a/packages/object-store/deploy/schemas/object_store_public/procedures/insert_nodes_at_paths.sql b/packages/object-store/deploy/schemas/object_store_public/procedures/insert_nodes_at_paths.sql new file mode 100644 index 000000000..2bca0171f --- /dev/null +++ b/packages/object-store/deploy/schemas/object_store_public/procedures/insert_nodes_at_paths.sql @@ -0,0 +1,465 @@ +-- Deploy schemas/object_store_public/procedures/insert_nodes_at_paths to pg + +-- requires: schemas/object_store_public/schema +-- requires: schemas/object_store_public/tables/object/table +-- requires: schemas/object_store_private/procedures/node_hash_uuid +-- requires: schemas/object_store_utils/procedures/array_index_of +-- requires: schemas/object_store_utils/procedures/array_utils + +BEGIN; + +-- Batched write primitive: apply a whole set of writes in one bottom-up pass. +-- +-- The store is content addressed, so writing a leaf cannot mutate its parent — +-- it mints a new parent, a new grandparent, and so on up to the root. Applying +-- N writes one at a time therefore costs N x depth node hashes and throws away +-- every root but the last. This pass hashes each dirty node exactly once: +-- +-- 1. stage the leaves (every write whose path is not also a directory), +-- 2. derive every dirty ancestor directory, +-- 3. resolve each dirty directory's existing node top-down from the current +-- root, so a dirty directory MERGES with the children it already has +-- instead of orphaning them, +-- 4. build level by level from the deepest up. +-- +-- Node ids are pure content hashes — no timestamps, no sequences, no insertion +-- order — so the root this returns is byte-identical to the one the eager +-- singular path produces for the same set of writes. +-- +-- Paths are a jsonb array of path arrays; kids_list / ktree_list are optional +-- parallel jsonb arrays carrying each write's explicit children. Duplicate +-- paths within one batch are last-write-wins. A path that is also a prefix of +-- another path in the batch supplies that directory's data and base children +-- and the deeper writes merge on top — the result the eager path gives when the +-- shallower write is applied first. +-- +-- The batch is carried in parallel arrays, unnested and joined, rather than in +-- keyed jsonb objects. Both are "one pass", but a per-row lookup into a +-- container holding the whole batch is not free: subscripting an N-element +-- jsonb[] walks it, so `datas[ord]` alone made the pass quadratic — 3.1s of a +-- 40k-path batch against 0.24s for the same values reached through +-- `unnest(datas) WITH ORDINALITY`. Directory children stay jsonb objects: that +-- is where the merge with a directory's existing children happens, the object's +-- key order is what fixes ktree/kids order and therefore the node hash, and +-- there are only ever as many directories as the tree is wide, not as many as +-- the batch is large. +CREATE FUNCTION object_store_public.insert_nodes_at_paths (s_id uuid, root uuid, paths jsonb, datas jsonb[], kids_list jsonb DEFAULT NULL, ktree_list jsonb DEFAULT NULL) + RETURNS uuid + AS $$ +DECLARE + -- every write, deduplicated: key, depth, own name, parent's key, then the + -- written content. Written content stays jsonb (including a JSON null when + -- the caller passed nothing) so "no children given" and "empty children + -- given" remain distinguishable, as they hash differently. + w_key text[]; + w_depth int[]; + w_name text[]; + w_parent text[]; + w_data jsonb[]; + w_kids jsonb[]; + w_ktree jsonb[]; + -- every dirty directory: every proper prefix of a written path, plus the + -- root, which every write dirties + d_key text[]; + d_depth int[]; + d_name text[]; + d_parent text[]; + -- the pre-existing node id of each dirty directory that already exists. + -- Only the id: a wide directory's children are materialised once, inside the + -- level query that needs them, never copied through a variable. + b_key text[] := '{}'; + b_id uuid[] := '{}'; + -- the leaves, already collapsed into one child map per parent directory. The + -- level loop then never touches anything batch-sized: a directory takes its + -- children from its own leaf map plus the directories built one level below. + lp_parent text[]; + lp_children jsonb[]; + -- the writes that are themselves dirty directories — a written path with + -- deeper writes under it. As many as the batch is deep, not as long as it is, + -- so the level loop can carry them. + dw_key text[]; + dw_data jsonb[]; + dw_kids jsonb[]; + dw_ktree jsonb[]; + -- the directories built by the previous (deeper) iteration + pd_name text[] := '{}'; + pd_parent text[] := '{}'; + pd_id uuid[] := '{}'; + -- the directories built by the current iteration + l_name text[]; + l_parent text[]; + l_id uuid[]; + root_key CONSTANT text := '[]'; + root_id uuid; + max_depth int; + cur_depth int; +BEGIN + IF (paths IS NULL OR jsonb_array_length(paths) = 0) THEN + RETURN root; + END IF; + + -- 1+2. normalise the writes (the last duplicate of a path wins), then derive + -- every dirty directory: every proper prefix of a written path, plus the + -- root, which every write dirties + WITH exploded AS ( + SELECT + ARRAY ( + SELECT + jsonb_array_elements_text(e.value))::text[] AS path, + e.ord::int AS ord, + d.data + FROM jsonb_array_elements(insert_nodes_at_paths.paths) WITH ORDINALITY AS e (value, ord) + LEFT JOIN unnest(insert_nodes_at_paths.datas) WITH ORDINALITY AS d (data, ord) ON d.ord = e.ord +), + writes AS ( + SELECT DISTINCT ON (to_jsonb(x.path)::text) + to_jsonb(x.path)::text AS node_key, + x.path, + cardinality(x.path) AS depth, + x.path[cardinality(x.path)] AS name, + to_jsonb(x.path[1:cardinality(x.path) - 1])::text AS parent, + x.data, + coalesce(insert_nodes_at_paths.kids_list -> (x.ord - 1), 'null'::jsonb) AS kids, + coalesce(insert_nodes_at_paths.ktree_list -> (x.ord - 1), 'null'::jsonb) AS ktree + FROM exploded AS x + ORDER BY + to_jsonb(x.path)::text, + x.ord DESC +), + dirs AS ( + SELECT DISTINCT + to_jsonb(pfx.path)::text AS node_key, + cardinality(pfx.path) AS depth, + pfx.path[cardinality(pfx.path)] AS name, + to_jsonb(pfx.path[1:cardinality(pfx.path) - 1])::text AS parent + FROM ( + SELECT + ARRAY[]::text[] AS path + UNION ALL + SELECT + wr.path[1:g.i] + FROM writes AS wr, + LATERAL generate_series(1, wr.depth - 1) AS g (i)) AS pfx +) +-- Each group of parallel arrays is aggregated in ONE pass, so every array in +-- the group sees the same row order and stays aligned with its siblings. + SELECT + wa.keys, + wa.depths, + wa.names, + wa.parents, + wa.datas, + wa.kids, + wa.ktree, + da.keys, + da.depths, + da.names, + da.parents, + da.max_depth + FROM ( + SELECT + array_agg(wr.node_key) AS keys, + array_agg(wr.depth) AS depths, + array_agg(wr.name) AS names, + array_agg(wr.parent) AS parents, + array_agg(wr.data) AS datas, + array_agg(wr.kids) AS kids, + array_agg(wr.ktree) AS ktree + FROM writes AS wr) AS wa, + ( + SELECT + array_agg(dr.node_key) AS keys, + array_agg(dr.depth) AS depths, + array_agg(dr.name) AS names, + array_agg(dr.parent) AS parents, + max(dr.depth) AS max_depth + FROM dirs AS dr) AS da INTO w_key, + w_depth, + w_name, + w_parent, + w_data, + w_kids, + w_ktree, + d_key, + d_depth, + d_name, + d_parent, + max_depth; + + -- 3. resolve each dirty directory against the pre-existing tree, walking down + -- from the current root so untouched siblings survive the rebuild + WITH RECURSIVE dirs AS ( + SELECT + dr.node_key, + dr.depth, + ARRAY ( + SELECT + jsonb_array_elements_text(dr.node_key::jsonb))::text[] AS path + FROM unnest(d_key, d_depth) AS dr (node_key, depth) +), + resolved AS ( + SELECT + root_key AS node_key, + 0 AS depth, + ARRAY[]::text[] AS path, + insert_nodes_at_paths.root AS node_id + UNION ALL + SELECT + child.node_key, + child.depth, + child.path, + parent_obj.kids[object_store_utils.array_index_of (parent_obj.ktree, child.path[child.depth])] + FROM resolved AS r + JOIN dirs AS child ON child.depth = r.depth + 1 + AND child.path[1:r.depth] = r.path + LEFT JOIN object_store_public.object AS parent_obj ON parent_obj.id = r.node_id + AND parent_obj.scope_id = insert_nodes_at_paths.s_id +) + SELECT + coalesce(array_agg(res.node_key), '{}'), + coalesce(array_agg(res.node_id), '{}') + FROM resolved AS res + WHERE res.node_id IS NOT NULL INTO b_key, + b_id; + + -- 4a. stage the leaves: writes that are not themselves dirty directories. + -- Their kids/ktree are inserted exactly as given, so a caller passing + -- empty arrays still hashes the same as the singular path does. Leaves are + -- the numerous, narrow nodes in a batch, so they go in with one set-based + -- insert; naming them in their parents' child maps needs their ids up + -- front, which is what node_hash_uuid computes. + WITH split AS ( + SELECT + wr.*, + EXISTS ( + SELECT + 1 + FROM unnest(d_key) AS dr (node_key) + WHERE dr.node_key = wr.node_key) AS is_dir + FROM unnest(w_key, w_depth, w_name, w_parent, w_data, w_kids, w_ktree) AS wr (node_key, depth, name, parent, data, kids, ktree) +), + staged AS ( + SELECT + wr.name, + wr.parent, + wr.data, + CASE WHEN jsonb_typeof(wr.kids) = 'array' THEN + ARRAY ( + SELECT + jsonb_array_elements_text(wr.kids))::uuid[] + END AS kids, + CASE WHEN jsonb_typeof(wr.ktree) = 'array' THEN + ARRAY ( + SELECT + jsonb_array_elements_text(wr.ktree))::text[] + END AS ktree + FROM split AS wr + WHERE NOT wr.is_dir +), + with_ids AS ( + SELECT + s.name, + s.parent, + s.data, + s.kids, + s.ktree, + object_store_private.node_hash_uuid (s.data, s.kids, s.ktree) AS node_id + FROM staged AS s +), + inserted AS ( + INSERT INTO object_store_public.object (scope_id, data, kids, ktree) + SELECT + insert_nodes_at_paths.s_id, + d.data, + d.kids, + d.ktree + FROM ( + SELECT DISTINCT ON (w.node_id) + w.node_id, + w.data, + w.kids, + w.ktree + FROM with_ids AS w + ORDER BY + w.node_id) AS d + ON CONFLICT (id, scope_id) + DO UPDATE SET + scope_id = EXCLUDED.scope_id + RETURNING + id +) + SELECT + la.parents, + la.children, + da.keys, + da.datas, + da.kids, + da.ktree + FROM ( + SELECT + coalesce(array_agg(g.parent), '{}') AS parents, + coalesce(array_agg(g.children), '{}') AS children + FROM ( + SELECT + w.parent, + jsonb_object_agg(w.name, to_jsonb(w.node_id)) AS children + FROM with_ids AS w + GROUP BY + w.parent) AS g) AS la, + ( + SELECT + coalesce(array_agg(wr.node_key), '{}') AS keys, + coalesce(array_agg(wr.data), '{}') AS datas, + coalesce(array_agg(wr.kids), '{}') AS kids, + coalesce(array_agg(wr.ktree), '{}') AS ktree + FROM split AS wr + WHERE wr.is_dir) AS da INTO lp_parent, + lp_children, + dw_key, + dw_data, + dw_kids, + dw_ktree; + + -- 4b. build every dirty directory level by level, deepest first. One level is + -- one set-based insert: nothing at the same depth can be another's child, + -- so a level's nodes are independent, and node_hash_uuid gives their ids + -- without a row to read them back from. + FOR cur_depth IN REVERSE max_depth..0 LOOP + WITH level AS ( + SELECT + dr.node_key, + dr.name, + dr.parent + FROM unnest(d_key, d_depth, d_name, d_parent) AS dr (node_key, depth, name, parent) + WHERE dr.depth = cur_depth +), + dir_kids AS ( + SELECT + pd.parent AS parent_key, + jsonb_object_agg(pd.name, to_jsonb(pd.node_id)) AS children + FROM unnest(pd_name, pd_parent, pd_id) AS pd (name, parent, node_id) + GROUP BY + pd.parent +), + merged AS ( + SELECT + l.node_key, + l.name, + l.parent, + CASE WHEN wr.node_key IS NOT NULL THEN + wr.data + ELSE + base_obj.data + END AS data, + CASE WHEN jsonb_typeof(wr.ktree) = 'array' THEN + ARRAY ( + SELECT + jsonb_array_elements_text(wr.ktree))::text[] + END AS raw_ktree, + CASE WHEN jsonb_typeof(wr.kids) = 'array' THEN + ARRAY ( + SELECT + jsonb_array_elements_text(wr.kids))::uuid[] + END AS raw_kids, + -- a write at this path replaces the directory's data and base children; + -- otherwise the pre-existing node's children are the base to merge into + (CASE WHEN wr.node_key IS NOT NULL THEN + coalesce(object_store_utils.zip_arrays ( + CASE WHEN jsonb_typeof(wr.ktree) = 'array' THEN + ARRAY ( + SELECT + jsonb_array_elements_text(wr.ktree))::text[] + END, + CASE WHEN jsonb_typeof(wr.kids) = 'array' THEN + ARRAY ( + SELECT + jsonb_array_elements_text(wr.kids))::uuid[] + END), '{}'::jsonb) + ELSE + coalesce(object_store_utils.zip_arrays (base_obj.ktree, base_obj.kids), '{}'::jsonb) + END) || coalesce(lk.children, '{}'::jsonb) || coalesce(dk.children, '{}'::jsonb) AS children, + -- a written path with no dirty child below it is a plain leaf write: + -- keep its kids/ktree verbatim so empty arrays stay empty arrays and + -- hash exactly as the singular path's direct insert does + (wr.node_key IS NOT NULL + AND lk.children IS NULL + AND dk.children IS NULL) AS keep_raw_children + FROM level AS l + LEFT JOIN unnest(dw_key, dw_data, dw_kids, dw_ktree) AS wr (node_key, data, kids, ktree) ON wr.node_key = l.node_key + LEFT JOIN unnest(lp_parent, lp_children) AS lk (parent_key, children) ON lk.parent_key = l.node_key + LEFT JOIN dir_kids AS dk ON dk.parent_key = l.node_key + LEFT JOIN unnest(b_key, b_id) AS bs (node_key, node_id) ON bs.node_key = l.node_key + LEFT JOIN object_store_public.object AS base_obj ON base_obj.id = bs.node_id + AND base_obj.scope_id = insert_nodes_at_paths.s_id +), + built AS ( + SELECT + m.name, + m.parent, + m.data, + CASE WHEN m.keep_raw_children THEN + m.raw_ktree + ELSE + u.ktree + END AS ktree, + CASE WHEN m.keep_raw_children THEN + m.raw_kids + ELSE + u.kids + END AS kids + FROM merged AS m, + LATERAL object_store_utils.unzip_obj_to_ktree_and_kids (m.children) AS u +), + with_ids AS ( + SELECT + b.name, + b.parent, + b.data, + b.kids, + b.ktree, + object_store_private.node_hash_uuid (b.data, b.kids, b.ktree) AS node_id + FROM built AS b +), + inserted AS ( + INSERT INTO object_store_public.object (scope_id, data, kids, ktree) + SELECT + insert_nodes_at_paths.s_id, + d.data, + d.kids, + d.ktree + FROM ( + SELECT DISTINCT ON (w.node_id) + w.node_id, + w.data, + w.kids, + w.ktree + FROM with_ids AS w + ORDER BY + w.node_id) AS d + ON CONFLICT (id, scope_id) + DO UPDATE SET + scope_id = EXCLUDED.scope_id + RETURNING + id +) + SELECT + coalesce(array_agg(w.name), '{}'), + coalesce(array_agg(w.parent), '{}'), + coalesce(array_agg(w.node_id), '{}') + FROM with_ids AS w INTO l_name, + l_parent, + l_id; + pd_name := l_name; + pd_parent := l_parent; + pd_id := l_id; + END LOOP; + + -- depth 0 is the root, and it is a single node: the last level built is it + root_id := pd_id[1]; + RETURN root_id; +END; +$$ +LANGUAGE plpgsql +VOLATILE; + +COMMIT; diff --git a/packages/object-store/deploy/schemas/object_store_utils/procedures/array_utils.sql b/packages/object-store/deploy/schemas/object_store_utils/procedures/array_utils.sql index 1d8a113da..327e29697 100644 --- a/packages/object-store/deploy/schemas/object_store_utils/procedures/array_utils.sql +++ b/packages/object-store/deploy/schemas/object_store_utils/procedures/array_utils.sql @@ -9,18 +9,32 @@ CREATE FUNCTION object_store_utils.zip_arrays( b anyarray ) returns jsonb as $$ DECLARE - obj jsonb = '{}'::jsonb; - i int; + obj jsonb; BEGIN IF (cardinality(a) != cardinality(b)) THEN RAISE EXCEPTION 'cannot zip arrays of different cardinality'; END IF; - - FOR i IN - SELECT * FROM generate_series(1, cardinality(a)) - LOOP - obj = jsonb_set(obj, ARRAY[a[i]]::text[], to_jsonb( (b[i]::text) ) ); - END LOOP; + + -- A null key is a hard error and a null value makes the whole object + -- null, as the equivalent per-element jsonb_set chain did (a null path + -- element raises; to_jsonb and jsonb_set are strict). + IF EXISTS ( + SELECT 1 FROM generate_series(1, cardinality(a)) AS i WHERE a[i] IS NULL + ) THEN + RAISE EXCEPTION 'cannot zip arrays with a null key'; + END IF; + + IF EXISTS ( + SELECT 1 FROM generate_series(1, cardinality(a)) AS i WHERE b[i] IS NULL + ) THEN + RETURN NULL; + END IF; + + -- Later duplicate keys win, matching the assignment order of the loop. + SELECT coalesce(jsonb_object_agg(a[i], to_jsonb(b[i]::text) ORDER BY i), '{}'::jsonb) + INTO obj + FROM generate_series(1, cardinality(a)) AS i; + RETURN obj; END; $$ @@ -29,17 +43,13 @@ LANGUAGE 'plpgsql' IMMUTABLE; CREATE FUNCTION object_store_utils.unzip_obj_to_ktree_and_kids( obj jsonb ) returns table ( ktree text[], kids uuid[] ) as $$ -DECLARE - key text; - value text; BEGIN - FOR key, value IN - SELECT * FROM jsonb_each_text(obj) - LOOP - ktree = array_append(ktree, key); - kids = array_append(kids, value::uuid); - END LOOP; - + -- Aggregated in jsonb_each_text's own emission order, the order the + -- per-key append loop produced; both arrays feed the node hash. + SELECT array_agg(e.key), array_agg(e.value::uuid) + INTO ktree, kids + FROM jsonb_each_text(obj) AS e; + RETURN next; END; $$ diff --git a/packages/object-store/object-store.control b/packages/object-store/object-store.control index ab72679bd..063376b2e 100644 --- a/packages/object-store/object-store.control +++ b/packages/object-store/object-store.control @@ -1,6 +1,6 @@ # object-store extension comment = 'object-store extension - content-addressable Merkle tree storage' -default_version = '0.36.0' +default_version = '0.39.0' module_pathname = '$libdir/object-store' requires = 'plpgsql,pgcrypto,uuid-ossp,pgpm-verify' relocatable = false diff --git a/packages/object-store/package.json b/packages/object-store/package.json index 8de8659b6..409163ce3 100644 --- a/packages/object-store/package.json +++ b/packages/object-store/package.json @@ -1,6 +1,6 @@ { "name": "@pgpm/object-store", - "version": "0.36.0", + "version": "0.39.0", "description": "Immutable versioned object storage with content-addressable IDs", "author": "Dan Lynch ", "contributors": [ diff --git a/packages/object-store/pgpm.plan b/packages/object-store/pgpm.plan index ee2e39101..82dbba255 100644 --- a/packages/object-store/pgpm.plan +++ b/packages/object-store/pgpm.plan @@ -26,3 +26,5 @@ schemas/object_store_public/tables/object/indexes/frzn_idx [schemas/object_store schemas/object_store_public/tables/object/indexes/object_kids_idx [schemas/object_store_public/schema schemas/object_store_public/tables/object/table] 2017-08-11T08:11:51Z skitch # add schemas/object_store_public/tables/object/indexes/object_kids_idx schemas/object_store_public/tables/object/triggers/generate_id_hash [schemas/object_store_private/schema schemas/object_store_public/tables/object/table schemas/object_store_private/procedures/object_hash_uuid] 2017-08-11T08:11:51Z skitch # add schemas/object_store_public/tables/object/triggers/generate_id_hash schemas/object_store_public/tables/object/triggers/immutable_objects [schemas/object_store_private/schema schemas/object_store_public/tables/object/table] 2017-08-11T08:11:51Z skitch # add schemas/object_store_public/tables/object/triggers/immutable_objects +schemas/object_store_private/procedures/node_hash_uuid [schemas/object_store_private/schema schemas/object_store_public/tables/object/table schemas/object_store_private/procedures/object_hash_uuid] 2026-08-06T00:00:00Z devin # add schemas/object_store_private/procedures/node_hash_uuid +schemas/object_store_public/procedures/insert_nodes_at_paths [schemas/object_store_public/schema schemas/object_store_private/procedures/node_hash_uuid schemas/object_store_public/tables/object/table schemas/object_store_utils/procedures/array_index_of schemas/object_store_utils/procedures/array_utils] 2026-08-06T00:00:00Z devin # add schemas/object_store_public/procedures/insert_nodes_at_paths diff --git a/packages/object-store/revert/schemas/object_store_private/procedures/node_hash_uuid.sql b/packages/object-store/revert/schemas/object_store_private/procedures/node_hash_uuid.sql new file mode 100644 index 000000000..3a4ef6da0 --- /dev/null +++ b/packages/object-store/revert/schemas/object_store_private/procedures/node_hash_uuid.sql @@ -0,0 +1,7 @@ +-- Revert schemas/object_store_private/procedures/node_hash_uuid from pg + +BEGIN; + +DROP FUNCTION object_store_private.node_hash_uuid(jsonb, uuid[], text[]); + +COMMIT; diff --git a/packages/object-store/revert/schemas/object_store_private/procedures/object_hash_uuid.sql b/packages/object-store/revert/schemas/object_store_private/procedures/object_hash_uuid.sql index faea3f923..c998047f3 100644 --- a/packages/object-store/revert/schemas/object_store_private/procedures/object_hash_uuid.sql +++ b/packages/object-store/revert/schemas/object_store_private/procedures/object_hash_uuid.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION object_store_public.object_hash_uuid; +DROP FUNCTION object_store_public.object_hash_uuid(object_store_public.object); COMMIT; diff --git a/packages/object-store/revert/schemas/object_store_public/procedures/freeze_objects.sql b/packages/object-store/revert/schemas/object_store_public/procedures/freeze_objects.sql index ae727ee9b..59b9f3b22 100644 --- a/packages/object-store/revert/schemas/object_store_public/procedures/freeze_objects.sql +++ b/packages/object-store/revert/schemas/object_store_public/procedures/freeze_objects.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION object_store_public.freeze_objects; +DROP FUNCTION object_store_public.freeze_objects(uuid, uuid); COMMIT; diff --git a/packages/object-store/revert/schemas/object_store_public/procedures/get_all.sql b/packages/object-store/revert/schemas/object_store_public/procedures/get_all.sql index 62848b3f2..c62c93941 100644 --- a/packages/object-store/revert/schemas/object_store_public/procedures/get_all.sql +++ b/packages/object-store/revert/schemas/object_store_public/procedures/get_all.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION object_store_public.get_all; +DROP FUNCTION object_store_public.get_all(uuid, uuid); COMMIT; diff --git a/packages/object-store/revert/schemas/object_store_public/procedures/get_all_objects_from_root.sql b/packages/object-store/revert/schemas/object_store_public/procedures/get_all_objects_from_root.sql index 612c32078..739ed8b17 100644 --- a/packages/object-store/revert/schemas/object_store_public/procedures/get_all_objects_from_root.sql +++ b/packages/object-store/revert/schemas/object_store_public/procedures/get_all_objects_from_root.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION object_store_public.get_all_objects_from_root; +DROP FUNCTION object_store_public.get_all_objects_from_root(uuid, uuid); COMMIT; diff --git a/packages/object-store/revert/schemas/object_store_public/procedures/get_node_at_path.sql b/packages/object-store/revert/schemas/object_store_public/procedures/get_node_at_path.sql index be207df25..f28d1f678 100644 --- a/packages/object-store/revert/schemas/object_store_public/procedures/get_node_at_path.sql +++ b/packages/object-store/revert/schemas/object_store_public/procedures/get_node_at_path.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION object_store_public.get_node_at_path; +DROP FUNCTION object_store_public.get_node_at_path(uuid, uuid, text[]); COMMIT; diff --git a/packages/object-store/revert/schemas/object_store_public/procedures/get_path_objects_from_root.sql b/packages/object-store/revert/schemas/object_store_public/procedures/get_path_objects_from_root.sql index 5098b331f..fb00b1882 100644 --- a/packages/object-store/revert/schemas/object_store_public/procedures/get_path_objects_from_root.sql +++ b/packages/object-store/revert/schemas/object_store_public/procedures/get_path_objects_from_root.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION object_store_public.get_path_objects_from_root; +DROP FUNCTION object_store_public.get_path_objects_from_root(uuid, uuid, text[]); COMMIT; diff --git a/packages/object-store/revert/schemas/object_store_public/procedures/insert_node_at_path.sql b/packages/object-store/revert/schemas/object_store_public/procedures/insert_node_at_path.sql index ae06163e3..c7b95230e 100644 --- a/packages/object-store/revert/schemas/object_store_public/procedures/insert_node_at_path.sql +++ b/packages/object-store/revert/schemas/object_store_public/procedures/insert_node_at_path.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION object_store_public.insert_node_at_path; +DROP FUNCTION object_store_public.insert_node_at_path(uuid, uuid, text[], jsonb, uuid[], text[]); COMMIT; diff --git a/packages/object-store/revert/schemas/object_store_public/procedures/insert_nodes_at_paths.sql b/packages/object-store/revert/schemas/object_store_public/procedures/insert_nodes_at_paths.sql new file mode 100644 index 000000000..4e3edc232 --- /dev/null +++ b/packages/object-store/revert/schemas/object_store_public/procedures/insert_nodes_at_paths.sql @@ -0,0 +1,7 @@ +-- Revert schemas/object_store_public/procedures/insert_nodes_at_paths from pg + +BEGIN; + +DROP FUNCTION object_store_public.insert_nodes_at_paths(uuid, uuid, jsonb, jsonb[], jsonb, jsonb); + +COMMIT; diff --git a/packages/object-store/revert/schemas/object_store_public/procedures/remove_node_at_path.sql b/packages/object-store/revert/schemas/object_store_public/procedures/remove_node_at_path.sql index fbbba1772..9c05985d0 100644 --- a/packages/object-store/revert/schemas/object_store_public/procedures/remove_node_at_path.sql +++ b/packages/object-store/revert/schemas/object_store_public/procedures/remove_node_at_path.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION object_store_public.remove_node_at_path; +DROP FUNCTION object_store_public.remove_node_at_path(uuid, uuid, text[]); COMMIT; diff --git a/packages/object-store/revert/schemas/object_store_public/procedures/set_data_at_path.sql b/packages/object-store/revert/schemas/object_store_public/procedures/set_data_at_path.sql index 037fa7c83..2ea80148e 100644 --- a/packages/object-store/revert/schemas/object_store_public/procedures/set_data_at_path.sql +++ b/packages/object-store/revert/schemas/object_store_public/procedures/set_data_at_path.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION object_store_public.set_data_at_path; +DROP FUNCTION object_store_public.set_data_at_path(uuid, uuid, text[], jsonb); COMMIT; diff --git a/packages/object-store/revert/schemas/object_store_public/procedures/update_node_at_path.sql b/packages/object-store/revert/schemas/object_store_public/procedures/update_node_at_path.sql index 4d897cf53..713434340 100644 --- a/packages/object-store/revert/schemas/object_store_public/procedures/update_node_at_path.sql +++ b/packages/object-store/revert/schemas/object_store_public/procedures/update_node_at_path.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION object_store_public.update_node_at_path; +DROP FUNCTION object_store_public.update_node_at_path(uuid, uuid, text[], jsonb, uuid[], text[]); COMMIT; diff --git a/packages/object-store/revert/schemas/object_store_public/tables/object/triggers/generate_id_hash.sql b/packages/object-store/revert/schemas/object_store_public/tables/object/triggers/generate_id_hash.sql index 5177a0186..54500ce25 100644 --- a/packages/object-store/revert/schemas/object_store_public/tables/object/triggers/generate_id_hash.sql +++ b/packages/object-store/revert/schemas/object_store_public/tables/object/triggers/generate_id_hash.sql @@ -3,6 +3,6 @@ BEGIN; DROP TRIGGER generate_id_hash ON object_store_public.object; -DROP FUNCTION object_store_private.tg_generate_id_hash; +DROP FUNCTION object_store_private.tg_generate_id_hash(); COMMIT; diff --git a/packages/object-store/revert/schemas/object_store_public/tables/object/triggers/immutable_objects.sql b/packages/object-store/revert/schemas/object_store_public/tables/object/triggers/immutable_objects.sql index aa5566a8b..eef09208c 100644 --- a/packages/object-store/revert/schemas/object_store_public/tables/object/triggers/immutable_objects.sql +++ b/packages/object-store/revert/schemas/object_store_public/tables/object/triggers/immutable_objects.sql @@ -2,8 +2,8 @@ BEGIN; -DROP TRIGGER immutable_objects ON object_store_public.object; DROP TRIGGER delete_immutable_objects ON object_store_public.object; -DROP FUNCTION object_store_private.tg_immutable_objects; +DROP TRIGGER immutable_objects ON object_store_public.object; +DROP FUNCTION object_store_private.tg_immutable_objects(); COMMIT; diff --git a/packages/object-store/revert/schemas/object_store_utils/procedures/array_get_last.sql b/packages/object-store/revert/schemas/object_store_utils/procedures/array_get_last.sql index cd78950d6..48cfdc4ed 100644 --- a/packages/object-store/revert/schemas/object_store_utils/procedures/array_get_last.sql +++ b/packages/object-store/revert/schemas/object_store_utils/procedures/array_get_last.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION object_store_utils.array_get_last; +DROP FUNCTION object_store_utils.array_get_last(anyarray); COMMIT; diff --git a/packages/object-store/revert/schemas/object_store_utils/procedures/array_index_of.sql b/packages/object-store/revert/schemas/object_store_utils/procedures/array_index_of.sql index b1284ac45..09417cfa3 100644 --- a/packages/object-store/revert/schemas/object_store_utils/procedures/array_index_of.sql +++ b/packages/object-store/revert/schemas/object_store_utils/procedures/array_index_of.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION object_store_utils.array_index_of; +DROP FUNCTION object_store_utils.array_index_of(anyarray, anyelement); COMMIT; diff --git a/packages/object-store/revert/schemas/object_store_utils/procedures/array_pop.sql b/packages/object-store/revert/schemas/object_store_utils/procedures/array_pop.sql index 2d440801c..07c9e6206 100644 --- a/packages/object-store/revert/schemas/object_store_utils/procedures/array_pop.sql +++ b/packages/object-store/revert/schemas/object_store_utils/procedures/array_pop.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION object_store_utils.array_pop; +DROP FUNCTION object_store_utils.array_pop(anyarray); COMMIT; diff --git a/packages/object-store/revert/schemas/object_store_utils/procedures/array_shift.sql b/packages/object-store/revert/schemas/object_store_utils/procedures/array_shift.sql index ae2394e58..0f518bc28 100644 --- a/packages/object-store/revert/schemas/object_store_utils/procedures/array_shift.sql +++ b/packages/object-store/revert/schemas/object_store_utils/procedures/array_shift.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION object_store_utils.array_shift; +DROP FUNCTION object_store_utils.array_shift(anyarray); COMMIT; diff --git a/packages/object-store/revert/schemas/object_store_utils/procedures/array_utils.sql b/packages/object-store/revert/schemas/object_store_utils/procedures/array_utils.sql index fbc4f4a4e..e9559e904 100644 --- a/packages/object-store/revert/schemas/object_store_utils/procedures/array_utils.sql +++ b/packages/object-store/revert/schemas/object_store_utils/procedures/array_utils.sql @@ -2,7 +2,7 @@ BEGIN; -DROP FUNCTION object_store_utils.zip_arrays; -DROP FUNCTION object_store_utils.unzip_obj_to_ktree_and_kids; +DROP FUNCTION object_store_utils.unzip_obj_to_ktree_and_kids(jsonb); +DROP FUNCTION object_store_utils.zip_arrays(text[], anyarray); COMMIT; diff --git a/packages/object-store/sql/object-store--0.36.0.bundle.tar.gz b/packages/object-store/sql/object-store--0.36.0.bundle.tar.gz deleted file mode 100644 index 14b61fa04..000000000 Binary files a/packages/object-store/sql/object-store--0.36.0.bundle.tar.gz and /dev/null differ diff --git a/packages/object-store/sql/object-store--0.36.0.sql b/packages/object-store/sql/object-store--0.36.0.sql deleted file mode 100644 index 79ffdbdcf..000000000 --- a/packages/object-store/sql/object-store--0.36.0.sql +++ /dev/null @@ -1,685 +0,0 @@ -\echo Use "CREATE EXTENSION object-store" to load this file. \quit -CREATE SCHEMA object_store_private; - -GRANT USAGE ON SCHEMA object_store_private TO authenticated; - -ALTER DEFAULT PRIVILEGES IN SCHEMA object_store_private - GRANT EXECUTE ON FUNCTIONS TO authenticated; - -ALTER DEFAULT PRIVILEGES IN SCHEMA object_store_private - GRANT ALL ON SEQUENCES TO authenticated; - -ALTER DEFAULT PRIVILEGES IN SCHEMA object_store_private - GRANT ALL ON TABLES TO authenticated; - -CREATE SCHEMA object_store_public; - -GRANT USAGE ON SCHEMA object_store_public TO authenticated; - -ALTER DEFAULT PRIVILEGES IN SCHEMA object_store_public - GRANT EXECUTE ON FUNCTIONS TO authenticated; - -ALTER DEFAULT PRIVILEGES IN SCHEMA object_store_public - GRANT ALL ON SEQUENCES TO authenticated; - -ALTER DEFAULT PRIVILEGES IN SCHEMA object_store_public - GRANT ALL ON TABLES TO authenticated; - -CREATE SCHEMA object_store_utils; - -GRANT USAGE ON SCHEMA object_store_utils TO authenticated; - -ALTER DEFAULT PRIVILEGES IN SCHEMA object_store_utils - GRANT EXECUTE ON FUNCTIONS TO authenticated; - -ALTER DEFAULT PRIVILEGES IN SCHEMA object_store_utils - GRANT ALL ON SEQUENCES TO authenticated; - -ALTER DEFAULT PRIVILEGES IN SCHEMA object_store_utils - GRANT ALL ON TABLES TO authenticated; - -CREATE FUNCTION object_store_utils.array_get_last( - arr anyarray -) RETURNS anyelement AS $EOFCODE$ - SELECT arr[array_length(arr, 1)]; -$EOFCODE$ LANGUAGE sql IMMUTABLE; - -CREATE FUNCTION object_store_utils.array_index_of( - arr anyarray, - el anyelement -) RETURNS int AS $EOFCODE$ -DECLARE - val int = -1; - i int; -BEGIN - FOR i IN SELECT * FROM generate_subscripts(arr, 1) g(i) - LOOP - IF (el = arr[i]) THEN - val = i; - RETURN val; - END IF; - END LOOP; - RETURN val; -END -$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; - -CREATE FUNCTION object_store_utils.array_pop( - srcarr anyarray -) RETURNS SETOF anyarray AS $EOFCODE$ -SELECT ARRAY ( - SELECT UNNEST(srcarr) LIMIT ( - SELECT array_upper(srcarr, 1) - 1 - ) -) -$EOFCODE$ LANGUAGE sql IMMUTABLE; - -CREATE FUNCTION object_store_utils.array_shift( - srcarr anyarray -) RETURNS SETOF anyarray AS $EOFCODE$ -SELECT srcarr[2:array_length(srcarr, 1)] -$EOFCODE$ LANGUAGE sql IMMUTABLE; - -CREATE FUNCTION object_store_utils.zip_arrays( - a text[], - b anyarray -) RETURNS jsonb AS $EOFCODE$ -DECLARE - obj jsonb = '{}'::jsonb; - i int; -BEGIN - IF (cardinality(a) != cardinality(b)) THEN - RAISE EXCEPTION 'cannot zip arrays of different cardinality'; - END IF; - - FOR i IN - SELECT * FROM generate_series(1, cardinality(a)) - LOOP - obj = jsonb_set(obj, ARRAY[a[i]]::text[], to_jsonb( (b[i]::text) ) ); - END LOOP; - RETURN obj; -END; -$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; - -CREATE FUNCTION object_store_utils.unzip_obj_to_ktree_and_kids( - obj jsonb -) RETURNS TABLE ( - ktree text[], - kids uuid[] -) AS $EOFCODE$ -DECLARE - key text; - value text; -BEGIN - FOR key, value IN - SELECT * FROM jsonb_each_text(obj) - LOOP - ktree = array_append(ktree, key); - kids = array_append(kids, value::uuid); - END LOOP; - - RETURN next; -END; -$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; - -CREATE TABLE object_store_public.object ( - id uuid NOT NULL, - scope_id uuid NOT NULL, - kids uuid[], - ktree text[], - data jsonb, - frzn bool DEFAULT false, - created_at timestamptz DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (id, scope_id), - CONSTRAINT kids_ktree_length_match - CHECK ( - cardinality(kids) = cardinality(ktree) - OR (kids IS NULL - AND ktree IS NULL) - ) -); - -CREATE FUNCTION object_store_public.get_all_objects_from_root( - s_id uuid, - id uuid -) RETURNS SETOF object_store_public.object AS $EOFCODE$ WITH RECURSIVE hierarchy AS ( - SELECT - * - FROM - object_store_public.object o - WHERE - o.id = get_all_objects_from_root.id AND o.scope_id=s_id - UNION - SELECT - object.* - FROM - object_store_public.object AS object - JOIN hierarchy a ON (object.id = ANY (a.kids) AND object.scope_id=a.scope_id)) -SELECT - * -FROM - hierarchy; -$EOFCODE$ LANGUAGE sql STABLE; - -CREATE FUNCTION object_store_public.get_all( - s_id uuid, - id uuid -) RETURNS TABLE ( - path text[], - data jsonb -) AS $EOFCODE$ -DECLARE - root object_store_public.object; - pth text[]; - i int; - - cid uuid; - cname text; - - rpath text[]; - rdata jsonb; -BEGIN - - SELECT * from object_store_public.object o WHERE o.scope_id = s_id - AND o.id = get_all.id - INTO root; - - pth = ARRAY[]::text[]; - - FOR i IN - SELECT * FROM generate_series(1, cardinality(root.kids)) - LOOP - cid = root.kids[i]; - cname = root.ktree[i]; - - FOR rpath, rdata IN - SELECT * FROM object_store_public.get_all(s_id, cid) - LOOP - path := ARRAY[cname] || rpath; - data := rdata; - RETURN next; - END LOOP; - - END LOOP; - - path := ARRAY[]::text[]; - data := root.data; - RETURN next; - - -END; -$EOFCODE$ LANGUAGE plpgsql STABLE; - -CREATE FUNCTION object_store_public.get_node_at_path( - s_id uuid, - id uuid, - path text[] DEFAULT CAST(ARRAY[] AS text[]) -) RETURNS object_store_public.object AS $EOFCODE$ - -DECLARE - _path text[] = path; - _obj object_store_public.object; - - i int; - pos int; - curpath text; - - _node text; - _node_id uuid; -BEGIN - - SELECT * FROM object_store_public.object o - WHERE o.id = get_node_at_path.id - AND o.scope_id = s_id - INTO _obj; - - IF (array_length(_path, 1) > 0) THEN - FOR i IN SELECT * FROM generate_subscripts(_path, 1) g(i) - LOOP - - curpath = _path[1]; - pos = object_store_utils.array_index_of(_obj.ktree, curpath); - - IF (pos > 0) THEN - _node_id = _obj.kids[pos]; - SELECT * FROM object_store_public.object o - WHERE o.id = _node_id - AND o.scope_id = s_id - INTO _obj; - -- TODO check if 1 is correct - -- NOTE is only NULL, not 0 for whatver reason if you need to use that... - -- IF (array_length(_path, 1) IS NULL) THEN - IF (array_length(_path, 1) = 1) THEN - RETURN _obj; - END IF; - - END IF; - _path = object_store_utils.array_shift(_path); - END LOOP; - ELSE - RETURN _obj; - END IF; - - RETURN NULL; -END; -$EOFCODE$ LANGUAGE plpgsql STABLE; - -CREATE FUNCTION object_store_public.get_path_objects_from_root( - s_id uuid, - id uuid, - path text[] DEFAULT CAST(ARRAY[] AS text[]) -) RETURNS SETOF object_store_public.object AS $EOFCODE$ - -DECLARE - _path text[] = path; - _obj object_store_public.object; - - i int; - pos int; - curpath text; - - _node text; - _node_id uuid; -BEGIN - - SELECT * FROM object_store_public.object o - WHERE o.id = get_path_objects_from_root.id - AND o.scope_id = s_id - INTO _obj; - RETURN NEXT _obj; - - FOR i IN SELECT * FROM generate_subscripts(_path, 1) g(i) - LOOP - - curpath = _path[1]; - pos = object_store_utils.array_index_of(_obj.ktree, curpath); - - IF (pos > 0) THEN - _node_id = _obj.kids[pos]; - SELECT * FROM object_store_public.object o - WHERE o.id = _node_id - AND o.scope_id = s_id - INTO _obj; - RETURN NEXT _obj; - - END IF; - _path = object_store_utils.array_shift(_path); - END LOOP; - -END; -$EOFCODE$ LANGUAGE plpgsql STABLE; - -CREATE FUNCTION object_store_public.insert_node_at_path( - s_id uuid, - root uuid, - path text[], - data jsonb, - kids uuid[], - ktree text[] -) RETURNS uuid AS $EOFCODE$ -DECLARE - _newnode_id uuid; - _newparent_id uuid; - _parent object_store_public.object; - _orig_name text; - _repl uuid; - _uuid_to_return uuid; - _parent_existed bool; - - vkids uuid[]; - vktree text[]; - - i int; - _path_len int; - _depth int; - _pos int; - _cur_id uuid; - _cur_obj object_store_public.object; - - -- Cached nodes along the path for O(D) bottom-up rebuild. - -- Index 1 = root (depth 0), index k+1 = node at path[1..k] (depth k). - _cached object_store_public.object[]; - - children_hash jsonb; - -BEGIN - - _path_len = coalesce(array_length(path, 1), 0); - - -- STEP 1: Walk down the path from root, caching each node. - -- Replaces O(D) call to get_node_at_path AND pre-populates the - -- cache so the bottom-up rebuild avoids re-walking from root - -- at each level (eliminates the O(D²) repeated walks). - - SELECT * FROM object_store_public.object o - WHERE o.id = insert_node_at_path.root - AND o.scope_id = s_id - INTO _cur_obj; - - _cached[1] = _cur_obj; - _depth = 0; - - IF (_cur_obj.id IS NOT NULL AND _path_len > 0) THEN - FOR i IN 1.._path_len LOOP - _pos = object_store_utils.array_index_of(_cur_obj.ktree, path[i]); - IF (_pos > 0) THEN - _cur_id = _cur_obj.kids[_pos]; - SELECT * FROM object_store_public.object o - WHERE o.id = _cur_id - AND o.scope_id = s_id - INTO _cur_obj; - _cached[i + 1] = _cur_obj; - _depth = i; - ELSE - EXIT; - END IF; - END LOOP; - END IF; - - -- STEP 2: Insert the new node - INSERT INTO object_store_public.object (scope_id, data, kids, ktree) - VALUES (s_id, insert_node_at_path.data, insert_node_at_path.kids, insert_node_at_path.ktree) - ON CONFLICT (id, scope_id) - DO UPDATE -- DO NOT USE NOTHING! won't return an ID - SET scope_id = EXCLUDED.scope_id - RETURNING - id - INTO _newnode_id; - - IF (_newnode_id IS NULL) THEN - RAISE EXCEPTION '_newnode_id failed'; - END IF; - - _orig_name = path[_path_len]; - _repl = _newnode_id; - _uuid_to_return = _newnode_id; - - -- STEP 3: Walk back up, creating new parent nodes with updated children. - -- Each parent is read from _cached instead of re-walking from root. - FOR i IN REVERSE _path_len..1 LOOP - - -- Parent is at depth i-1, stored in _cached[i] - IF (i - 1 = 0) THEN - _parent = _cached[1]; - _parent_existed = ((_parent).id IS NOT NULL); - ELSIF (i - 1 <= _depth) THEN - _parent = _cached[i]; - _parent_existed = TRUE; - ELSE - _parent_existed = FALSE; - END IF; - - IF (_parent_existed) THEN - - -- Update parent's children: jsonb_set replaces if key exists, adds if not - children_hash = object_store_utils.zip_arrays( - _parent.ktree, - _parent.kids - ); - - children_hash = jsonb_set(children_hash, ARRAY[_orig_name]::text[], to_jsonb(_repl)); - - SELECT h.ktree, h.kids FROM object_store_utils.unzip_obj_to_ktree_and_kids( - children_hash - ) h INTO vktree, vkids; - - INSERT INTO object_store_public.object (scope_id, data, kids, ktree) - VALUES (s_id, _parent.data, vkids, vktree) - ON CONFLICT (id, scope_id) - DO UPDATE -- DO NOT USE NOTHING! won't return an ID - SET scope_id = EXCLUDED.scope_id - RETURNING - id - INTO _newparent_id; - - IF (_newparent_id IS NULL) THEN - RAISE EXCEPTION 'parent insert failed at depth %', i - 1; - END IF; - - ELSE - - -- Parent doesn't exist: create new node with single child - INSERT INTO object_store_public.object (scope_id, data, kids, ktree) - VALUES (s_id, NULL, ARRAY[_repl]::uuid[], ARRAY[_orig_name]::text[]) - ON CONFLICT (id, scope_id) - DO UPDATE -- DO NOT USE NOTHING! won't return an ID - SET scope_id = EXCLUDED.scope_id - RETURNING - id - INTO _newparent_id; - - END IF; - - -- Move up: this parent becomes the child for the next level - _orig_name = path[i - 1]; - _repl = _newparent_id; - _uuid_to_return = _newparent_id; - - END LOOP; - - RETURN _uuid_to_return; -END; -$EOFCODE$ LANGUAGE plpgsql VOLATILE; - -CREATE FUNCTION object_store_public.object_hash_uuid( - obj object_store_public.object -) RETURNS uuid AS $EOFCODE$ -DECLARE - _cash jsonb = '{}'::jsonb; - hash1 uuid; - hash2 uuid; -BEGIN - IF (obj.data IS NOT NULL) THEN - hash1 = uuid_generate_v5 (uuid_ns_url (), obj.data::text); - END IF; - - IF (obj.kids IS NOT NULL AND obj.ktree IS NOT NULL) THEN - -- TODO for future feature, this is where you can put an IF statement to allow order. Not sure where to put the meta data for allowing order, but could be as simple as a bool field on all objects called "order" - -- _cash is the children hash, it is ordered using jsonb lexically - _cash = json_object(obj.ktree::text[], obj.kids::text[]); - hash2 = uuid_generate_v5 (uuid_ns_url (), _cash::text); - END IF; - - RETURN uuid_generate_v5 (uuid_ns_url (), concat(hash1, hash2)::text); -END; -$EOFCODE$ LANGUAGE plpgsql STABLE; - -CREATE FUNCTION object_store_public.remove_node_at_path( - s_id uuid, - root uuid, - path text[] -) RETURNS uuid AS $EOFCODE$ -DECLARE - _node object_store_public.object; - _parent object_store_public.object; - - _newparent_id uuid; - _path text[] = path; - - child_to_remove text; - - vkids uuid[]; - vktree text[]; - children_hash jsonb; -BEGIN - - IF (cardinality(path) < 1) THEN - RAISE EXCEPTION 'cannot remove root node'; - END IF; - - - -- STEP 1 - -- check if it exists - SELECT - * - FROM - object_store_public.get_node_at_path - (s_id, root, path) - INTO _node; - - -- NOTE cannot use FOUND/NOT FOUND here - IF (_node.id IS NULL) THEN - RETURN root; - END IF; - - -- STEP 2(a) get child to remove - child_to_remove = object_store_utils.array_get_last(_path); - - -- STEP 2(b) get parent - _path = object_store_utils.array_pop(_path); - - SELECT - * - FROM - object_store_public.get_node_at_path - (s_id, root, _path) - INTO _parent; - - children_hash = object_store_utils.zip_arrays( - _parent.ktree, - _parent.kids - ); - - children_hash = children_hash - child_to_remove; - - SELECT h.ktree, h.kids FROM object_store_utils.unzip_obj_to_ktree_and_kids( - children_hash - ) h INTO vktree, vkids; - - - -- STEP 3 update new parent - RETURN object_store_public.insert_node_at_path( - s_id, - root, - _path, - _parent.data, - vkids, - vktree - ); - -END; -$EOFCODE$ LANGUAGE plpgsql VOLATILE; - -CREATE FUNCTION object_store_public.update_node_at_path( - s_id uuid, - root uuid, - path text[], - data jsonb, - kids uuid[], - ktree text[] -) RETURNS uuid AS $EOFCODE$ -BEGIN - RETURN object_store_public.insert_node_at_path(s_id, root, path, data, kids, ktree); -END; -$EOFCODE$ LANGUAGE plpgsql VOLATILE; - -CREATE FUNCTION object_store_public.set_data_at_path( - s_id uuid, - root uuid, - path text[], - data jsonb -) RETURNS uuid AS $EOFCODE$ -DECLARE - _node object_store_public.object; - _kids uuid[] = ARRAY[]::uuid[]; - _ktree text[] = ARRAY[]::text[]; -BEGIN - -- Look up existing node to preserve its children - SELECT * FROM object_store_public.get_node_at_path(s_id, root, path) - INTO _node; - - IF (_node.id IS NOT NULL) THEN - _kids = _node.kids; - _ktree = _node.ktree; - END IF; - - -- Delegate to insert_node_at_path with preserved children - RETURN object_store_public.insert_node_at_path( - s_id, root, path, data, _kids, _ktree - ); -END; -$EOFCODE$ LANGUAGE plpgsql VOLATILE; - -CREATE FUNCTION object_store_public.freeze_objects( - s_id uuid, - id uuid -) RETURNS void AS $EOFCODE$ -BEGIN - --- Unnest kids so each recursion step joins through the (id, scope_id) --- primary key instead of scanning the whole table per level. -WITH RECURSIVE hierarchy AS ( - SELECT - o.id, o.scope_id, o.kids - FROM - object_store_public.object o - WHERE - o.id = freeze_objects.id AND o.scope_id=s_id - UNION - SELECT - object.id, object.scope_id, object.kids - FROM - hierarchy a - CROSS JOIN LATERAL unnest(a.kids) AS kid(id) - JOIN object_store_public.object AS object - ON (object.id = kid.id AND object.scope_id=a.scope_id)) - -UPDATE object_store_public.object o - SET frzn = TRUE -FROM hierarchy - WHERE hierarchy.id = o.id AND hierarchy.scope_id = o.scope_id; - -END; -$EOFCODE$ LANGUAGE plpgsql VOLATILE; - -CREATE INDEX scope_id_idx ON object_store_public.object (scope_id); - -CREATE INDEX frzn_idx ON object_store_public.object (frzn); - -CREATE INDEX object_kids_idx ON object_store_public.object USING gin (kids); - -CREATE FUNCTION object_store_private.tg_generate_id_hash() RETURNS trigger AS $EOFCODE$ -BEGIN - NEW.id = object_store_public.object_hash_uuid (NEW); - RETURN NEW; -END; -$EOFCODE$ LANGUAGE plpgsql VOLATILE; - -CREATE TRIGGER generate_id_hash - BEFORE INSERT - ON object_store_public.object - FOR EACH ROW - EXECUTE PROCEDURE object_store_private.tg_generate_id_hash(); - -CREATE FUNCTION object_store_private.tg_immutable_objects() RETURNS trigger AS $EOFCODE$ -BEGIN - IF tg_op = 'UPDATE' THEN - IF (OLD.frzn IS FALSE AND NEW.frzn IS TRUE) THEN - -- that's ok... - ELSE - RAISE EXCEPTION 'you cannot mutate an immutable record.'; - END IF; - END IF; - IF tg_op = 'DELETE' THEN - IF (OLD.frzn IS TRUE) THEN - RAISE EXCEPTION 'you cannot delete an immutable record.'; - END IF; - END IF; - RETURN NEW; -END; -$EOFCODE$ LANGUAGE plpgsql; - -CREATE TRIGGER immutable_objects - BEFORE UPDATE - ON object_store_public.object - FOR EACH ROW - WHEN (new.id <> old.id - OR new.data <> old.data - OR new.kids <> old.kids - OR new.ktree <> old.ktree) - EXECUTE PROCEDURE object_store_private.tg_immutable_objects(); - -CREATE TRIGGER delete_immutable_objects - BEFORE DELETE - ON object_store_public.object - FOR EACH ROW - EXECUTE PROCEDURE object_store_private.tg_immutable_objects(); \ No newline at end of file diff --git a/packages/object-store/sql/object-store--0.39.0.bundle.tar.gz b/packages/object-store/sql/object-store--0.39.0.bundle.tar.gz new file mode 100644 index 000000000..c7402b604 Binary files /dev/null and b/packages/object-store/sql/object-store--0.39.0.bundle.tar.gz differ diff --git a/packages/object-store/sql/object-store--0.39.0.sql b/packages/object-store/sql/object-store--0.39.0.sql new file mode 100644 index 000000000..26dcf3ecc --- /dev/null +++ b/packages/object-store/sql/object-store--0.39.0.sql @@ -0,0 +1,987 @@ +\echo Use "CREATE EXTENSION object-store" to load this file. \quit +CREATE SCHEMA object_store_private; + +GRANT USAGE ON SCHEMA object_store_private TO authenticated; + +ALTER DEFAULT PRIVILEGES IN SCHEMA object_store_private + GRANT EXECUTE ON FUNCTIONS TO authenticated; + +ALTER DEFAULT PRIVILEGES IN SCHEMA object_store_private + GRANT ALL ON SEQUENCES TO authenticated; + +ALTER DEFAULT PRIVILEGES IN SCHEMA object_store_private + GRANT ALL ON TABLES TO authenticated; + +CREATE SCHEMA object_store_public; + +GRANT USAGE ON SCHEMA object_store_public TO authenticated; + +ALTER DEFAULT PRIVILEGES IN SCHEMA object_store_public + GRANT EXECUTE ON FUNCTIONS TO authenticated; + +ALTER DEFAULT PRIVILEGES IN SCHEMA object_store_public + GRANT ALL ON SEQUENCES TO authenticated; + +ALTER DEFAULT PRIVILEGES IN SCHEMA object_store_public + GRANT ALL ON TABLES TO authenticated; + +CREATE SCHEMA object_store_utils; + +GRANT USAGE ON SCHEMA object_store_utils TO authenticated; + +ALTER DEFAULT PRIVILEGES IN SCHEMA object_store_utils + GRANT EXECUTE ON FUNCTIONS TO authenticated; + +ALTER DEFAULT PRIVILEGES IN SCHEMA object_store_utils + GRANT ALL ON SEQUENCES TO authenticated; + +ALTER DEFAULT PRIVILEGES IN SCHEMA object_store_utils + GRANT ALL ON TABLES TO authenticated; + +CREATE FUNCTION object_store_utils.array_get_last( + arr anyarray +) RETURNS anyelement AS $EOFCODE$ + SELECT arr[array_length(arr, 1)]; +$EOFCODE$ LANGUAGE sql IMMUTABLE; + +CREATE FUNCTION object_store_utils.array_index_of( + arr anyarray, + el anyelement +) RETURNS int AS $EOFCODE$ +DECLARE + val int = -1; + i int; +BEGIN + FOR i IN SELECT * FROM generate_subscripts(arr, 1) g(i) + LOOP + IF (el = arr[i]) THEN + val = i; + RETURN val; + END IF; + END LOOP; + RETURN val; +END +$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; + +CREATE FUNCTION object_store_utils.array_pop( + srcarr anyarray +) RETURNS SETOF anyarray AS $EOFCODE$ +SELECT ARRAY ( + SELECT UNNEST(srcarr) LIMIT ( + SELECT array_upper(srcarr, 1) - 1 + ) +) +$EOFCODE$ LANGUAGE sql IMMUTABLE; + +CREATE FUNCTION object_store_utils.array_shift( + srcarr anyarray +) RETURNS SETOF anyarray AS $EOFCODE$ +SELECT srcarr[2:array_length(srcarr, 1)] +$EOFCODE$ LANGUAGE sql IMMUTABLE; + +CREATE FUNCTION object_store_utils.zip_arrays( + a text[], + b anyarray +) RETURNS jsonb AS $EOFCODE$ +DECLARE + obj jsonb; +BEGIN + IF (cardinality(a) != cardinality(b)) THEN + RAISE EXCEPTION 'cannot zip arrays of different cardinality'; + END IF; + + -- A null key is a hard error and a null value makes the whole object + -- null, as the equivalent per-element jsonb_set chain did (a null path + -- element raises; to_jsonb and jsonb_set are strict). + IF EXISTS ( + SELECT 1 FROM generate_series(1, cardinality(a)) AS i WHERE a[i] IS NULL + ) THEN + RAISE EXCEPTION 'cannot zip arrays with a null key'; + END IF; + + IF EXISTS ( + SELECT 1 FROM generate_series(1, cardinality(a)) AS i WHERE b[i] IS NULL + ) THEN + RETURN NULL; + END IF; + + -- Later duplicate keys win, matching the assignment order of the loop. + SELECT coalesce(jsonb_object_agg(a[i], to_jsonb(b[i]::text) ORDER BY i), '{}'::jsonb) + INTO obj + FROM generate_series(1, cardinality(a)) AS i; + + RETURN obj; +END; +$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; + +CREATE FUNCTION object_store_utils.unzip_obj_to_ktree_and_kids( + obj jsonb +) RETURNS TABLE ( + ktree text[], + kids uuid[] +) AS $EOFCODE$ +BEGIN + -- Aggregated in jsonb_each_text's own emission order, the order the + -- per-key append loop produced; both arrays feed the node hash. + SELECT array_agg(e.key), array_agg(e.value::uuid) + INTO ktree, kids + FROM jsonb_each_text(obj) AS e; + + RETURN next; +END; +$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; + +CREATE TABLE object_store_public.object ( + id uuid NOT NULL, + scope_id uuid NOT NULL, + kids uuid[], + ktree text[], + data jsonb, + frzn bool DEFAULT false, + created_at timestamptz DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (id, scope_id), + CONSTRAINT kids_ktree_length_match + CHECK ( + cardinality(kids) = cardinality(ktree) + OR (kids IS NULL + AND ktree IS NULL) + ) +); + +CREATE FUNCTION object_store_public.get_all_objects_from_root( + s_id uuid, + id uuid +) RETURNS SETOF object_store_public.object AS $EOFCODE$ WITH RECURSIVE hierarchy AS ( + SELECT + * + FROM + object_store_public.object o + WHERE + o.id = get_all_objects_from_root.id AND o.scope_id=s_id + UNION + SELECT + object.* + FROM + object_store_public.object AS object + JOIN hierarchy a ON (object.id = ANY (a.kids) AND object.scope_id=a.scope_id)) +SELECT + * +FROM + hierarchy; +$EOFCODE$ LANGUAGE sql STABLE; + +CREATE FUNCTION object_store_public.get_all( + s_id uuid, + id uuid +) RETURNS TABLE ( + path text[], + data jsonb +) AS $EOFCODE$ +DECLARE + root object_store_public.object; + pth text[]; + i int; + + cid uuid; + cname text; + + rpath text[]; + rdata jsonb; +BEGIN + + SELECT * from object_store_public.object o WHERE o.scope_id = s_id + AND o.id = get_all.id + INTO root; + + pth = ARRAY[]::text[]; + + FOR i IN + SELECT * FROM generate_series(1, cardinality(root.kids)) + LOOP + cid = root.kids[i]; + cname = root.ktree[i]; + + FOR rpath, rdata IN + SELECT * FROM object_store_public.get_all(s_id, cid) + LOOP + path := ARRAY[cname] || rpath; + data := rdata; + RETURN next; + END LOOP; + + END LOOP; + + path := ARRAY[]::text[]; + data := root.data; + RETURN next; + + +END; +$EOFCODE$ LANGUAGE plpgsql STABLE; + +CREATE FUNCTION object_store_public.get_node_at_path( + s_id uuid, + id uuid, + path text[] DEFAULT CAST(ARRAY[] AS text[]) +) RETURNS object_store_public.object AS $EOFCODE$ + +DECLARE + _path text[] = path; + _obj object_store_public.object; + + i int; + pos int; + curpath text; + + _node text; + _node_id uuid; +BEGIN + + SELECT * FROM object_store_public.object o + WHERE o.id = get_node_at_path.id + AND o.scope_id = s_id + INTO _obj; + + IF (array_length(_path, 1) > 0) THEN + FOR i IN SELECT * FROM generate_subscripts(_path, 1) g(i) + LOOP + + curpath = _path[1]; + pos = object_store_utils.array_index_of(_obj.ktree, curpath); + + IF (pos > 0) THEN + _node_id = _obj.kids[pos]; + SELECT * FROM object_store_public.object o + WHERE o.id = _node_id + AND o.scope_id = s_id + INTO _obj; + -- TODO check if 1 is correct + -- NOTE is only NULL, not 0 for whatver reason if you need to use that... + -- IF (array_length(_path, 1) IS NULL) THEN + IF (array_length(_path, 1) = 1) THEN + RETURN _obj; + END IF; + + END IF; + _path = object_store_utils.array_shift(_path); + END LOOP; + ELSE + RETURN _obj; + END IF; + + RETURN NULL; +END; +$EOFCODE$ LANGUAGE plpgsql STABLE; + +CREATE FUNCTION object_store_public.get_path_objects_from_root( + s_id uuid, + id uuid, + path text[] DEFAULT CAST(ARRAY[] AS text[]) +) RETURNS SETOF object_store_public.object AS $EOFCODE$ + +DECLARE + _path text[] = path; + _obj object_store_public.object; + + i int; + pos int; + curpath text; + + _node text; + _node_id uuid; +BEGIN + + SELECT * FROM object_store_public.object o + WHERE o.id = get_path_objects_from_root.id + AND o.scope_id = s_id + INTO _obj; + RETURN NEXT _obj; + + FOR i IN SELECT * FROM generate_subscripts(_path, 1) g(i) + LOOP + + curpath = _path[1]; + pos = object_store_utils.array_index_of(_obj.ktree, curpath); + + IF (pos > 0) THEN + _node_id = _obj.kids[pos]; + SELECT * FROM object_store_public.object o + WHERE o.id = _node_id + AND o.scope_id = s_id + INTO _obj; + RETURN NEXT _obj; + + END IF; + _path = object_store_utils.array_shift(_path); + END LOOP; + +END; +$EOFCODE$ LANGUAGE plpgsql STABLE; + +CREATE FUNCTION object_store_public.insert_node_at_path( + s_id uuid, + root uuid, + path text[], + data jsonb, + kids uuid[], + ktree text[] +) RETURNS uuid AS $EOFCODE$ +BEGIN + RETURN object_store_public.insert_nodes_at_paths (s_id := insert_node_at_path.s_id, root := insert_node_at_path.root, paths := jsonb_build_array(coalesce(to_jsonb(insert_node_at_path.path), '[]'::jsonb)), datas := ARRAY[insert_node_at_path.data]::jsonb[], kids_list := jsonb_build_array(to_jsonb(insert_node_at_path.kids)), ktree_list := jsonb_build_array(to_jsonb(insert_node_at_path.ktree))); +END; +$EOFCODE$ LANGUAGE plpgsql VOLATILE; + +CREATE FUNCTION object_store_public.object_hash_uuid( + obj object_store_public.object +) RETURNS uuid AS $EOFCODE$ +DECLARE + _cash jsonb = '{}'::jsonb; + hash1 uuid; + hash2 uuid; +BEGIN + IF (obj.data IS NOT NULL) THEN + hash1 = uuid_generate_v5 (uuid_ns_url (), obj.data::text); + END IF; + + IF (obj.kids IS NOT NULL AND obj.ktree IS NOT NULL) THEN + -- TODO for future feature, this is where you can put an IF statement to allow order. Not sure where to put the meta data for allowing order, but could be as simple as a bool field on all objects called "order" + -- _cash is the children hash, it is ordered using jsonb lexically + _cash = json_object(obj.ktree::text[], obj.kids::text[]); + hash2 = uuid_generate_v5 (uuid_ns_url (), _cash::text); + END IF; + + RETURN uuid_generate_v5 (uuid_ns_url (), concat(hash1, hash2)::text); +END; +$EOFCODE$ LANGUAGE plpgsql STABLE; + +CREATE FUNCTION object_store_public.remove_node_at_path( + s_id uuid, + root uuid, + path text[] +) RETURNS uuid AS $EOFCODE$ +DECLARE + _node object_store_public.object; + _parent object_store_public.object; + + _newparent_id uuid; + _path text[] = path; + + child_to_remove text; + + vkids uuid[]; + vktree text[]; + children_hash jsonb; +BEGIN + + IF (cardinality(path) < 1) THEN + RAISE EXCEPTION 'cannot remove root node'; + END IF; + + + -- STEP 1 + -- check if it exists + SELECT + * + FROM + object_store_public.get_node_at_path + (s_id, root, path) + INTO _node; + + -- NOTE cannot use FOUND/NOT FOUND here + IF (_node.id IS NULL) THEN + RETURN root; + END IF; + + -- STEP 2(a) get child to remove + child_to_remove = object_store_utils.array_get_last(_path); + + -- STEP 2(b) get parent + _path = object_store_utils.array_pop(_path); + + SELECT + * + FROM + object_store_public.get_node_at_path + (s_id, root, _path) + INTO _parent; + + children_hash = object_store_utils.zip_arrays( + _parent.ktree, + _parent.kids + ); + + children_hash = children_hash - child_to_remove; + + SELECT h.ktree, h.kids FROM object_store_utils.unzip_obj_to_ktree_and_kids( + children_hash + ) h INTO vktree, vkids; + + + -- STEP 3 update new parent + RETURN object_store_public.insert_node_at_path( + s_id, + root, + _path, + _parent.data, + vkids, + vktree + ); + +END; +$EOFCODE$ LANGUAGE plpgsql VOLATILE; + +CREATE FUNCTION object_store_public.update_node_at_path( + s_id uuid, + root uuid, + path text[], + data jsonb, + kids uuid[], + ktree text[] +) RETURNS uuid AS $EOFCODE$ +BEGIN + RETURN object_store_public.insert_node_at_path(s_id, root, path, data, kids, ktree); +END; +$EOFCODE$ LANGUAGE plpgsql VOLATILE; + +CREATE FUNCTION object_store_public.set_data_at_path( + s_id uuid, + root uuid, + path text[], + data jsonb +) RETURNS uuid AS $EOFCODE$ +DECLARE + _node object_store_public.object; + _kids uuid[] = ARRAY[]::uuid[]; + _ktree text[] = ARRAY[]::text[]; +BEGIN + -- Look up existing node to preserve its children + SELECT * FROM object_store_public.get_node_at_path(s_id, root, path) + INTO _node; + + IF (_node.id IS NOT NULL) THEN + _kids = _node.kids; + _ktree = _node.ktree; + END IF; + + -- Delegate to insert_node_at_path with preserved children + RETURN object_store_public.insert_node_at_path( + s_id, root, path, data, _kids, _ktree + ); +END; +$EOFCODE$ LANGUAGE plpgsql VOLATILE; + +CREATE FUNCTION object_store_public.freeze_objects( + s_id uuid, + id uuid +) RETURNS void AS $EOFCODE$ +BEGIN + +-- Unnest kids so each recursion step joins through the (id, scope_id) +-- primary key instead of scanning the whole table per level. +WITH RECURSIVE hierarchy AS ( + SELECT + o.id, o.scope_id, o.kids + FROM + object_store_public.object o + WHERE + o.id = freeze_objects.id AND o.scope_id=s_id + UNION + SELECT + object.id, object.scope_id, object.kids + FROM + hierarchy a + CROSS JOIN LATERAL unnest(a.kids) AS kid(id) + JOIN object_store_public.object AS object + ON (object.id = kid.id AND object.scope_id=a.scope_id)) + +UPDATE object_store_public.object o + SET frzn = TRUE +FROM hierarchy + WHERE hierarchy.id = o.id AND hierarchy.scope_id = o.scope_id; + +END; +$EOFCODE$ LANGUAGE plpgsql VOLATILE; + +CREATE INDEX scope_id_idx ON object_store_public.object (scope_id); + +CREATE INDEX frzn_idx ON object_store_public.object (frzn); + +CREATE INDEX object_kids_idx ON object_store_public.object USING gin (kids); + +CREATE FUNCTION object_store_private.tg_generate_id_hash() RETURNS trigger AS $EOFCODE$ +BEGIN + NEW.id = object_store_public.object_hash_uuid (NEW); + RETURN NEW; +END; +$EOFCODE$ LANGUAGE plpgsql VOLATILE; + +CREATE TRIGGER generate_id_hash + BEFORE INSERT + ON object_store_public.object + FOR EACH ROW + EXECUTE PROCEDURE object_store_private.tg_generate_id_hash(); + +CREATE FUNCTION object_store_private.tg_immutable_objects() RETURNS trigger AS $EOFCODE$ +BEGIN + IF tg_op = 'UPDATE' THEN + IF (OLD.frzn IS FALSE AND NEW.frzn IS TRUE) THEN + -- that's ok... + ELSE + RAISE EXCEPTION 'you cannot mutate an immutable record.'; + END IF; + END IF; + IF tg_op = 'DELETE' THEN + IF (OLD.frzn IS TRUE) THEN + RAISE EXCEPTION 'you cannot delete an immutable record.'; + END IF; + END IF; + RETURN NEW; +END; +$EOFCODE$ LANGUAGE plpgsql; + +CREATE TRIGGER immutable_objects + BEFORE UPDATE + ON object_store_public.object + FOR EACH ROW + WHEN (new.id <> old.id + OR new.data <> old.data + OR new.kids <> old.kids + OR new.ktree <> old.ktree) + EXECUTE PROCEDURE object_store_private.tg_immutable_objects(); + +CREATE TRIGGER delete_immutable_objects + BEFORE DELETE + ON object_store_public.object + FOR EACH ROW + EXECUTE PROCEDURE object_store_private.tg_immutable_objects(); + +CREATE FUNCTION object_store_private.node_hash_uuid( + data jsonb, + kids uuid[], + ktree text[] +) RETURNS uuid AS $EOFCODE$ + SELECT + object_store_public.object_hash_uuid (jsonb_populate_record(NULL::object_store_public.object, jsonb_build_object('data', node_hash_uuid.data, 'kids', to_jsonb(node_hash_uuid.kids), 'ktree', to_jsonb(node_hash_uuid.ktree)))); +$EOFCODE$ LANGUAGE sql IMMUTABLE; + +CREATE FUNCTION object_store_public.insert_nodes_at_paths( + s_id uuid, + root uuid, + paths jsonb, + datas jsonb[], + kids_list jsonb DEFAULT NULL, + ktree_list jsonb DEFAULT NULL +) RETURNS uuid AS $EOFCODE$ +DECLARE + -- every write, deduplicated: key, depth, own name, parent's key, then the + -- written content. Written content stays jsonb (including a JSON null when + -- the caller passed nothing) so "no children given" and "empty children + -- given" remain distinguishable, as they hash differently. + w_key text[]; + w_depth int[]; + w_name text[]; + w_parent text[]; + w_data jsonb[]; + w_kids jsonb[]; + w_ktree jsonb[]; + -- every dirty directory: every proper prefix of a written path, plus the + -- root, which every write dirties + d_key text[]; + d_depth int[]; + d_name text[]; + d_parent text[]; + -- the pre-existing node id of each dirty directory that already exists. + -- Only the id: a wide directory's children are materialised once, inside the + -- level query that needs them, never copied through a variable. + b_key text[] := '{}'; + b_id uuid[] := '{}'; + -- the leaves, already collapsed into one child map per parent directory. The + -- level loop then never touches anything batch-sized: a directory takes its + -- children from its own leaf map plus the directories built one level below. + lp_parent text[]; + lp_children jsonb[]; + -- the writes that are themselves dirty directories — a written path with + -- deeper writes under it. As many as the batch is deep, not as long as it is, + -- so the level loop can carry them. + dw_key text[]; + dw_data jsonb[]; + dw_kids jsonb[]; + dw_ktree jsonb[]; + -- the directories built by the previous (deeper) iteration + pd_name text[] := '{}'; + pd_parent text[] := '{}'; + pd_id uuid[] := '{}'; + -- the directories built by the current iteration + l_name text[]; + l_parent text[]; + l_id uuid[]; + root_key CONSTANT text := '[]'; + root_id uuid; + max_depth int; + cur_depth int; +BEGIN + IF (paths IS NULL OR jsonb_array_length(paths) = 0) THEN + RETURN root; + END IF; + + -- 1+2. normalise the writes (the last duplicate of a path wins), then derive + -- every dirty directory: every proper prefix of a written path, plus the + -- root, which every write dirties + WITH exploded AS ( + SELECT + ARRAY ( + SELECT + jsonb_array_elements_text(e.value))::text[] AS path, + e.ord::int AS ord, + d.data + FROM jsonb_array_elements(insert_nodes_at_paths.paths) WITH ORDINALITY AS e (value, ord) + LEFT JOIN unnest(insert_nodes_at_paths.datas) WITH ORDINALITY AS d (data, ord) ON d.ord = e.ord +), + writes AS ( + SELECT DISTINCT ON (to_jsonb(x.path)::text) + to_jsonb(x.path)::text AS node_key, + x.path, + cardinality(x.path) AS depth, + x.path[cardinality(x.path)] AS name, + to_jsonb(x.path[1:cardinality(x.path) - 1])::text AS parent, + x.data, + coalesce(insert_nodes_at_paths.kids_list -> (x.ord - 1), 'null'::jsonb) AS kids, + coalesce(insert_nodes_at_paths.ktree_list -> (x.ord - 1), 'null'::jsonb) AS ktree + FROM exploded AS x + ORDER BY + to_jsonb(x.path)::text, + x.ord DESC +), + dirs AS ( + SELECT DISTINCT + to_jsonb(pfx.path)::text AS node_key, + cardinality(pfx.path) AS depth, + pfx.path[cardinality(pfx.path)] AS name, + to_jsonb(pfx.path[1:cardinality(pfx.path) - 1])::text AS parent + FROM ( + SELECT + ARRAY[]::text[] AS path + UNION ALL + SELECT + wr.path[1:g.i] + FROM writes AS wr, + LATERAL generate_series(1, wr.depth - 1) AS g (i)) AS pfx +) +-- Each group of parallel arrays is aggregated in ONE pass, so every array in +-- the group sees the same row order and stays aligned with its siblings. + SELECT + wa.keys, + wa.depths, + wa.names, + wa.parents, + wa.datas, + wa.kids, + wa.ktree, + da.keys, + da.depths, + da.names, + da.parents, + da.max_depth + FROM ( + SELECT + array_agg(wr.node_key) AS keys, + array_agg(wr.depth) AS depths, + array_agg(wr.name) AS names, + array_agg(wr.parent) AS parents, + array_agg(wr.data) AS datas, + array_agg(wr.kids) AS kids, + array_agg(wr.ktree) AS ktree + FROM writes AS wr) AS wa, + ( + SELECT + array_agg(dr.node_key) AS keys, + array_agg(dr.depth) AS depths, + array_agg(dr.name) AS names, + array_agg(dr.parent) AS parents, + max(dr.depth) AS max_depth + FROM dirs AS dr) AS da INTO w_key, + w_depth, + w_name, + w_parent, + w_data, + w_kids, + w_ktree, + d_key, + d_depth, + d_name, + d_parent, + max_depth; + + -- 3. resolve each dirty directory against the pre-existing tree, walking down + -- from the current root so untouched siblings survive the rebuild + WITH RECURSIVE dirs AS ( + SELECT + dr.node_key, + dr.depth, + ARRAY ( + SELECT + jsonb_array_elements_text(dr.node_key::jsonb))::text[] AS path + FROM unnest(d_key, d_depth) AS dr (node_key, depth) +), + resolved AS ( + SELECT + root_key AS node_key, + 0 AS depth, + ARRAY[]::text[] AS path, + insert_nodes_at_paths.root AS node_id + UNION ALL + SELECT + child.node_key, + child.depth, + child.path, + parent_obj.kids[object_store_utils.array_index_of (parent_obj.ktree, child.path[child.depth])] + FROM resolved AS r + JOIN dirs AS child ON child.depth = r.depth + 1 + AND child.path[1:r.depth] = r.path + LEFT JOIN object_store_public.object AS parent_obj ON parent_obj.id = r.node_id + AND parent_obj.scope_id = insert_nodes_at_paths.s_id +) + SELECT + coalesce(array_agg(res.node_key), '{}'), + coalesce(array_agg(res.node_id), '{}') + FROM resolved AS res + WHERE res.node_id IS NOT NULL INTO b_key, + b_id; + + -- 4a. stage the leaves: writes that are not themselves dirty directories. + -- Their kids/ktree are inserted exactly as given, so a caller passing + -- empty arrays still hashes the same as the singular path does. Leaves are + -- the numerous, narrow nodes in a batch, so they go in with one set-based + -- insert; naming them in their parents' child maps needs their ids up + -- front, which is what node_hash_uuid computes. + WITH split AS ( + SELECT + wr.*, + EXISTS ( + SELECT + 1 + FROM unnest(d_key) AS dr (node_key) + WHERE dr.node_key = wr.node_key) AS is_dir + FROM unnest(w_key, w_depth, w_name, w_parent, w_data, w_kids, w_ktree) AS wr (node_key, depth, name, parent, data, kids, ktree) +), + staged AS ( + SELECT + wr.name, + wr.parent, + wr.data, + CASE WHEN jsonb_typeof(wr.kids) = 'array' THEN + ARRAY ( + SELECT + jsonb_array_elements_text(wr.kids))::uuid[] + END AS kids, + CASE WHEN jsonb_typeof(wr.ktree) = 'array' THEN + ARRAY ( + SELECT + jsonb_array_elements_text(wr.ktree))::text[] + END AS ktree + FROM split AS wr + WHERE NOT wr.is_dir +), + with_ids AS ( + SELECT + s.name, + s.parent, + s.data, + s.kids, + s.ktree, + object_store_private.node_hash_uuid (s.data, s.kids, s.ktree) AS node_id + FROM staged AS s +), + inserted AS ( + INSERT INTO object_store_public.object (scope_id, data, kids, ktree) + SELECT + insert_nodes_at_paths.s_id, + d.data, + d.kids, + d.ktree + FROM ( + SELECT DISTINCT ON (w.node_id) + w.node_id, + w.data, + w.kids, + w.ktree + FROM with_ids AS w + ORDER BY + w.node_id) AS d + ON CONFLICT (id, scope_id) + DO UPDATE SET + scope_id = EXCLUDED.scope_id + RETURNING + id +) + SELECT + la.parents, + la.children, + da.keys, + da.datas, + da.kids, + da.ktree + FROM ( + SELECT + coalesce(array_agg(g.parent), '{}') AS parents, + coalesce(array_agg(g.children), '{}') AS children + FROM ( + SELECT + w.parent, + jsonb_object_agg(w.name, to_jsonb(w.node_id)) AS children + FROM with_ids AS w + GROUP BY + w.parent) AS g) AS la, + ( + SELECT + coalesce(array_agg(wr.node_key), '{}') AS keys, + coalesce(array_agg(wr.data), '{}') AS datas, + coalesce(array_agg(wr.kids), '{}') AS kids, + coalesce(array_agg(wr.ktree), '{}') AS ktree + FROM split AS wr + WHERE wr.is_dir) AS da INTO lp_parent, + lp_children, + dw_key, + dw_data, + dw_kids, + dw_ktree; + + -- 4b. build every dirty directory level by level, deepest first. One level is + -- one set-based insert: nothing at the same depth can be another's child, + -- so a level's nodes are independent, and node_hash_uuid gives their ids + -- without a row to read them back from. + FOR cur_depth IN REVERSE max_depth..0 LOOP + WITH level AS ( + SELECT + dr.node_key, + dr.name, + dr.parent + FROM unnest(d_key, d_depth, d_name, d_parent) AS dr (node_key, depth, name, parent) + WHERE dr.depth = cur_depth +), + dir_kids AS ( + SELECT + pd.parent AS parent_key, + jsonb_object_agg(pd.name, to_jsonb(pd.node_id)) AS children + FROM unnest(pd_name, pd_parent, pd_id) AS pd (name, parent, node_id) + GROUP BY + pd.parent +), + merged AS ( + SELECT + l.node_key, + l.name, + l.parent, + CASE WHEN wr.node_key IS NOT NULL THEN + wr.data + ELSE + base_obj.data + END AS data, + CASE WHEN jsonb_typeof(wr.ktree) = 'array' THEN + ARRAY ( + SELECT + jsonb_array_elements_text(wr.ktree))::text[] + END AS raw_ktree, + CASE WHEN jsonb_typeof(wr.kids) = 'array' THEN + ARRAY ( + SELECT + jsonb_array_elements_text(wr.kids))::uuid[] + END AS raw_kids, + -- a write at this path replaces the directory's data and base children; + -- otherwise the pre-existing node's children are the base to merge into + (CASE WHEN wr.node_key IS NOT NULL THEN + coalesce(object_store_utils.zip_arrays ( + CASE WHEN jsonb_typeof(wr.ktree) = 'array' THEN + ARRAY ( + SELECT + jsonb_array_elements_text(wr.ktree))::text[] + END, + CASE WHEN jsonb_typeof(wr.kids) = 'array' THEN + ARRAY ( + SELECT + jsonb_array_elements_text(wr.kids))::uuid[] + END), '{}'::jsonb) + ELSE + coalesce(object_store_utils.zip_arrays (base_obj.ktree, base_obj.kids), '{}'::jsonb) + END) || coalesce(lk.children, '{}'::jsonb) || coalesce(dk.children, '{}'::jsonb) AS children, + -- a written path with no dirty child below it is a plain leaf write: + -- keep its kids/ktree verbatim so empty arrays stay empty arrays and + -- hash exactly as the singular path's direct insert does + (wr.node_key IS NOT NULL + AND lk.children IS NULL + AND dk.children IS NULL) AS keep_raw_children + FROM level AS l + LEFT JOIN unnest(dw_key, dw_data, dw_kids, dw_ktree) AS wr (node_key, data, kids, ktree) ON wr.node_key = l.node_key + LEFT JOIN unnest(lp_parent, lp_children) AS lk (parent_key, children) ON lk.parent_key = l.node_key + LEFT JOIN dir_kids AS dk ON dk.parent_key = l.node_key + LEFT JOIN unnest(b_key, b_id) AS bs (node_key, node_id) ON bs.node_key = l.node_key + LEFT JOIN object_store_public.object AS base_obj ON base_obj.id = bs.node_id + AND base_obj.scope_id = insert_nodes_at_paths.s_id +), + built AS ( + SELECT + m.name, + m.parent, + m.data, + CASE WHEN m.keep_raw_children THEN + m.raw_ktree + ELSE + u.ktree + END AS ktree, + CASE WHEN m.keep_raw_children THEN + m.raw_kids + ELSE + u.kids + END AS kids + FROM merged AS m, + LATERAL object_store_utils.unzip_obj_to_ktree_and_kids (m.children) AS u +), + with_ids AS ( + SELECT + b.name, + b.parent, + b.data, + b.kids, + b.ktree, + object_store_private.node_hash_uuid (b.data, b.kids, b.ktree) AS node_id + FROM built AS b +), + inserted AS ( + INSERT INTO object_store_public.object (scope_id, data, kids, ktree) + SELECT + insert_nodes_at_paths.s_id, + d.data, + d.kids, + d.ktree + FROM ( + SELECT DISTINCT ON (w.node_id) + w.node_id, + w.data, + w.kids, + w.ktree + FROM with_ids AS w + ORDER BY + w.node_id) AS d + ON CONFLICT (id, scope_id) + DO UPDATE SET + scope_id = EXCLUDED.scope_id + RETURNING + id +) + SELECT + coalesce(array_agg(w.name), '{}'), + coalesce(array_agg(w.parent), '{}'), + coalesce(array_agg(w.node_id), '{}') + FROM with_ids AS w INTO l_name, + l_parent, + l_id; + pd_name := l_name; + pd_parent := l_parent; + pd_id := l_id; + END LOOP; + + -- depth 0 is the root, and it is a single node: the last level built is it + root_id := pd_id[1]; + RETURN root_id; +END; +$EOFCODE$ LANGUAGE plpgsql VOLATILE; \ No newline at end of file diff --git a/packages/object-store/verify/schemas/object_store_private/procedures/node_hash_uuid.sql b/packages/object-store/verify/schemas/object_store_private/procedures/node_hash_uuid.sql new file mode 100644 index 000000000..5bf9fe028 --- /dev/null +++ b/packages/object-store/verify/schemas/object_store_private/procedures/node_hash_uuid.sql @@ -0,0 +1,7 @@ +-- Verify schemas/object_store_private/procedures/node_hash_uuid on pg + +BEGIN; + +SELECT assert_function('object_store_private.node_hash_uuid(jsonb, uuid[], text[])'::regprocedure); + +ROLLBACK; diff --git a/packages/object-store/verify/schemas/object_store_private/procedures/object_hash_uuid.sql b/packages/object-store/verify/schemas/object_store_private/procedures/object_hash_uuid.sql index dedcf41b3..773f14021 100644 --- a/packages/object-store/verify/schemas/object_store_private/procedures/object_hash_uuid.sql +++ b/packages/object-store/verify/schemas/object_store_private/procedures/object_hash_uuid.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('object_store_public.object_hash_uuid'); +SELECT assert_function('object_store_public.object_hash_uuid(object_store_public.object)'::regprocedure); ROLLBACK; diff --git a/packages/object-store/verify/schemas/object_store_private/schema.sql b/packages/object-store/verify/schemas/object_store_private/schema.sql index 249ec99be..8b13ca73e 100644 --- a/packages/object-store/verify/schemas/object_store_private/schema.sql +++ b/packages/object-store/verify/schemas/object_store_private/schema.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_schema ('object_store_private'); +SELECT assert_schema('object_store_private'::regnamespace); ROLLBACK; diff --git a/packages/object-store/verify/schemas/object_store_public/procedures/freeze_objects.sql b/packages/object-store/verify/schemas/object_store_public/procedures/freeze_objects.sql index f7fd3a417..b80073836 100644 --- a/packages/object-store/verify/schemas/object_store_public/procedures/freeze_objects.sql +++ b/packages/object-store/verify/schemas/object_store_public/procedures/freeze_objects.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('object_store_public.freeze_objects'); +SELECT assert_function('object_store_public.freeze_objects(uuid, uuid)'::regprocedure); ROLLBACK; diff --git a/packages/object-store/verify/schemas/object_store_public/procedures/get_all.sql b/packages/object-store/verify/schemas/object_store_public/procedures/get_all.sql index 2ead168c0..e2333a160 100644 --- a/packages/object-store/verify/schemas/object_store_public/procedures/get_all.sql +++ b/packages/object-store/verify/schemas/object_store_public/procedures/get_all.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('object_store_public.get_all'); +SELECT assert_function('object_store_public.get_all(uuid, uuid)'::regprocedure); ROLLBACK; diff --git a/packages/object-store/verify/schemas/object_store_public/procedures/get_all_objects_from_root.sql b/packages/object-store/verify/schemas/object_store_public/procedures/get_all_objects_from_root.sql index b840f41f8..22d779624 100644 --- a/packages/object-store/verify/schemas/object_store_public/procedures/get_all_objects_from_root.sql +++ b/packages/object-store/verify/schemas/object_store_public/procedures/get_all_objects_from_root.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('object_store_public.get_all_objects_from_root'); +SELECT assert_function('object_store_public.get_all_objects_from_root(uuid, uuid)'::regprocedure); ROLLBACK; diff --git a/packages/object-store/verify/schemas/object_store_public/procedures/get_node_at_path.sql b/packages/object-store/verify/schemas/object_store_public/procedures/get_node_at_path.sql index dbce9e5ab..c01cd6595 100644 --- a/packages/object-store/verify/schemas/object_store_public/procedures/get_node_at_path.sql +++ b/packages/object-store/verify/schemas/object_store_public/procedures/get_node_at_path.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('object_store_public.get_node_at_path'); +SELECT assert_function('object_store_public.get_node_at_path(uuid, uuid, text[])'::regprocedure); ROLLBACK; diff --git a/packages/object-store/verify/schemas/object_store_public/procedures/get_path_objects_from_root.sql b/packages/object-store/verify/schemas/object_store_public/procedures/get_path_objects_from_root.sql index 60e6d1011..76ba05106 100644 --- a/packages/object-store/verify/schemas/object_store_public/procedures/get_path_objects_from_root.sql +++ b/packages/object-store/verify/schemas/object_store_public/procedures/get_path_objects_from_root.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('object_store_public.get_path_objects_from_root'); +SELECT assert_function('object_store_public.get_path_objects_from_root(uuid, uuid, text[])'::regprocedure); ROLLBACK; diff --git a/packages/object-store/verify/schemas/object_store_public/procedures/insert_node_at_path.sql b/packages/object-store/verify/schemas/object_store_public/procedures/insert_node_at_path.sql index fff4f8872..e584b1aa6 100644 --- a/packages/object-store/verify/schemas/object_store_public/procedures/insert_node_at_path.sql +++ b/packages/object-store/verify/schemas/object_store_public/procedures/insert_node_at_path.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('object_store_public.insert_node_at_path'); +SELECT assert_function('object_store_public.insert_node_at_path(uuid, uuid, text[], jsonb, uuid[], text[])'::regprocedure); ROLLBACK; diff --git a/packages/object-store/verify/schemas/object_store_public/procedures/insert_nodes_at_paths.sql b/packages/object-store/verify/schemas/object_store_public/procedures/insert_nodes_at_paths.sql new file mode 100644 index 000000000..627a631ab --- /dev/null +++ b/packages/object-store/verify/schemas/object_store_public/procedures/insert_nodes_at_paths.sql @@ -0,0 +1,7 @@ +-- Verify schemas/object_store_public/procedures/insert_nodes_at_paths on pg + +BEGIN; + +SELECT assert_function('object_store_public.insert_nodes_at_paths(uuid, uuid, jsonb, jsonb[], jsonb, jsonb)'::regprocedure); + +ROLLBACK; diff --git a/packages/object-store/verify/schemas/object_store_public/procedures/remove_node_at_path.sql b/packages/object-store/verify/schemas/object_store_public/procedures/remove_node_at_path.sql index 310bac9ba..880b29334 100644 --- a/packages/object-store/verify/schemas/object_store_public/procedures/remove_node_at_path.sql +++ b/packages/object-store/verify/schemas/object_store_public/procedures/remove_node_at_path.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('object_store_public.remove_node_at_path'); +SELECT assert_function('object_store_public.remove_node_at_path(uuid, uuid, text[])'::regprocedure); ROLLBACK; diff --git a/packages/object-store/verify/schemas/object_store_public/procedures/set_data_at_path.sql b/packages/object-store/verify/schemas/object_store_public/procedures/set_data_at_path.sql index 8136503ef..b8f6535de 100644 --- a/packages/object-store/verify/schemas/object_store_public/procedures/set_data_at_path.sql +++ b/packages/object-store/verify/schemas/object_store_public/procedures/set_data_at_path.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('object_store_public.set_data_at_path'); +SELECT assert_function('object_store_public.set_data_at_path(uuid, uuid, text[], jsonb)'::regprocedure); ROLLBACK; diff --git a/packages/object-store/verify/schemas/object_store_public/procedures/update_node_at_path.sql b/packages/object-store/verify/schemas/object_store_public/procedures/update_node_at_path.sql index 57c5d42ef..078e89141 100644 --- a/packages/object-store/verify/schemas/object_store_public/procedures/update_node_at_path.sql +++ b/packages/object-store/verify/schemas/object_store_public/procedures/update_node_at_path.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('object_store_public.update_node_at_path'); +SELECT assert_function('object_store_public.update_node_at_path(uuid, uuid, text[], jsonb, uuid[], text[])'::regprocedure); ROLLBACK; diff --git a/packages/object-store/verify/schemas/object_store_public/schema.sql b/packages/object-store/verify/schemas/object_store_public/schema.sql index 7ff91a7e4..3eeb90b88 100644 --- a/packages/object-store/verify/schemas/object_store_public/schema.sql +++ b/packages/object-store/verify/schemas/object_store_public/schema.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_schema ('object_store_public'); +SELECT assert_schema('object_store_public'::regnamespace); ROLLBACK; diff --git a/packages/object-store/verify/schemas/object_store_public/tables/object/indexes/frzn_idx.sql b/packages/object-store/verify/schemas/object_store_public/tables/object/indexes/frzn_idx.sql index 30a5740f1..601279f93 100644 --- a/packages/object-store/verify/schemas/object_store_public/tables/object/indexes/frzn_idx.sql +++ b/packages/object-store/verify/schemas/object_store_public/tables/object/indexes/frzn_idx.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_index ('object_store_public.object', 'frzn_idx'); +SELECT assert_index('object_store_public.frzn_idx'::regclass, 'object_store_public.object'::regclass); ROLLBACK; diff --git a/packages/object-store/verify/schemas/object_store_public/tables/object/indexes/object_kids_idx.sql b/packages/object-store/verify/schemas/object_store_public/tables/object/indexes/object_kids_idx.sql index d7ea73b20..fc70bc080 100644 --- a/packages/object-store/verify/schemas/object_store_public/tables/object/indexes/object_kids_idx.sql +++ b/packages/object-store/verify/schemas/object_store_public/tables/object/indexes/object_kids_idx.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_index ('object_store_public.object', 'object_kids_idx'); +SELECT assert_index('object_store_public.object_kids_idx'::regclass, 'object_store_public.object'::regclass); ROLLBACK; diff --git a/packages/object-store/verify/schemas/object_store_public/tables/object/indexes/scope_id_idx.sql b/packages/object-store/verify/schemas/object_store_public/tables/object/indexes/scope_id_idx.sql index 4dac15ebb..f92469aad 100644 --- a/packages/object-store/verify/schemas/object_store_public/tables/object/indexes/scope_id_idx.sql +++ b/packages/object-store/verify/schemas/object_store_public/tables/object/indexes/scope_id_idx.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_index ('object_store_public.object', 'scope_id_idx'); +SELECT assert_index('object_store_public.scope_id_idx'::regclass, 'object_store_public.object'::regclass); ROLLBACK; diff --git a/packages/object-store/verify/schemas/object_store_public/tables/object/table.sql b/packages/object-store/verify/schemas/object_store_public/tables/object/table.sql index 27d0e9915..b017aaed5 100644 --- a/packages/object-store/verify/schemas/object_store_public/tables/object/table.sql +++ b/packages/object-store/verify/schemas/object_store_public/tables/object/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('object_store_public.object'); +SELECT assert_table('object_store_public.object'::regclass); ROLLBACK; diff --git a/packages/object-store/verify/schemas/object_store_public/tables/object/triggers/generate_id_hash.sql b/packages/object-store/verify/schemas/object_store_public/tables/object/triggers/generate_id_hash.sql index 585cd46db..c98616cf9 100644 --- a/packages/object-store/verify/schemas/object_store_public/tables/object/triggers/generate_id_hash.sql +++ b/packages/object-store/verify/schemas/object_store_public/tables/object/triggers/generate_id_hash.sql @@ -2,7 +2,7 @@ BEGIN; -SELECT verify_function ('object_store_private.tg_generate_id_hash'); -SELECT verify_trigger ('object_store_public.generate_id_hash'); +SELECT assert_function('object_store_private.tg_generate_id_hash()'::regprocedure); +SELECT assert_trigger('object_store_public.object'::regclass, 'generate_id_hash', 'object_store_private.tg_generate_id_hash'::regproc, 7); ROLLBACK; diff --git a/packages/object-store/verify/schemas/object_store_public/tables/object/triggers/immutable_objects.sql b/packages/object-store/verify/schemas/object_store_public/tables/object/triggers/immutable_objects.sql index e024038aa..42f320e4f 100644 --- a/packages/object-store/verify/schemas/object_store_public/tables/object/triggers/immutable_objects.sql +++ b/packages/object-store/verify/schemas/object_store_public/tables/object/triggers/immutable_objects.sql @@ -2,8 +2,8 @@ BEGIN; -SELECT verify_function ('object_store_private.tg_immutable_objects'); -SELECT verify_trigger ('object_store_public.immutable_objects'); -SELECT verify_trigger ('object_store_public.delete_immutable_objects'); +SELECT assert_function('object_store_private.tg_immutable_objects()'::regprocedure); +SELECT assert_trigger('object_store_public.object'::regclass, 'immutable_objects', 'object_store_private.tg_immutable_objects'::regproc, 19); +SELECT assert_trigger('object_store_public.object'::regclass, 'delete_immutable_objects', 'object_store_private.tg_immutable_objects'::regproc, 11); ROLLBACK; diff --git a/packages/object-store/verify/schemas/object_store_utils/procedures/array_get_last.sql b/packages/object-store/verify/schemas/object_store_utils/procedures/array_get_last.sql index 6e6ce218d..cfc3c3b40 100644 --- a/packages/object-store/verify/schemas/object_store_utils/procedures/array_get_last.sql +++ b/packages/object-store/verify/schemas/object_store_utils/procedures/array_get_last.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('object_store_utils.array_get_last'); +SELECT assert_function('object_store_utils.array_get_last(anyarray)'::regprocedure); ROLLBACK; diff --git a/packages/object-store/verify/schemas/object_store_utils/procedures/array_index_of.sql b/packages/object-store/verify/schemas/object_store_utils/procedures/array_index_of.sql index 8e0557512..b3d40a0e9 100644 --- a/packages/object-store/verify/schemas/object_store_utils/procedures/array_index_of.sql +++ b/packages/object-store/verify/schemas/object_store_utils/procedures/array_index_of.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('object_store_utils.array_index_of'); +SELECT assert_function('object_store_utils.array_index_of(anyarray, anyelement)'::regprocedure); ROLLBACK; diff --git a/packages/object-store/verify/schemas/object_store_utils/procedures/array_pop.sql b/packages/object-store/verify/schemas/object_store_utils/procedures/array_pop.sql index c062c8a0f..dec20691f 100644 --- a/packages/object-store/verify/schemas/object_store_utils/procedures/array_pop.sql +++ b/packages/object-store/verify/schemas/object_store_utils/procedures/array_pop.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('object_store_utils.array_pop'); +SELECT assert_function('object_store_utils.array_pop(anyarray)'::regprocedure); ROLLBACK; diff --git a/packages/object-store/verify/schemas/object_store_utils/procedures/array_shift.sql b/packages/object-store/verify/schemas/object_store_utils/procedures/array_shift.sql index 3035a3c50..d43f93ccc 100644 --- a/packages/object-store/verify/schemas/object_store_utils/procedures/array_shift.sql +++ b/packages/object-store/verify/schemas/object_store_utils/procedures/array_shift.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('object_store_utils.array_shift'); +SELECT assert_function('object_store_utils.array_shift(anyarray)'::regprocedure); ROLLBACK; diff --git a/packages/object-store/verify/schemas/object_store_utils/procedures/array_utils.sql b/packages/object-store/verify/schemas/object_store_utils/procedures/array_utils.sql index 2b4756291..394192158 100644 --- a/packages/object-store/verify/schemas/object_store_utils/procedures/array_utils.sql +++ b/packages/object-store/verify/schemas/object_store_utils/procedures/array_utils.sql @@ -2,7 +2,7 @@ BEGIN; -SELECT verify_function ('object_store_utils.zip_arrays'); -SELECT verify_function ('object_store_utils.unzip_obj_to_ktree_and_kids'); +SELECT assert_function('object_store_utils.zip_arrays(text[], anyarray)'::regprocedure); +SELECT assert_function('object_store_utils.unzip_obj_to_ktree_and_kids(jsonb)'::regprocedure); ROLLBACK; diff --git a/packages/object-store/verify/schemas/object_store_utils/schema.sql b/packages/object-store/verify/schemas/object_store_utils/schema.sql index ecb13cf9f..9411632ec 100644 --- a/packages/object-store/verify/schemas/object_store_utils/schema.sql +++ b/packages/object-store/verify/schemas/object_store_utils/schema.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_schema ('object_store_utils'); +SELECT assert_schema('object_store_utils'::regnamespace); ROLLBACK; diff --git a/packages/object-tree/Makefile b/packages/object-tree/Makefile index 003b0588f..35bab593e 100644 --- a/packages/object-tree/Makefile +++ b/packages/object-tree/Makefile @@ -1,5 +1,5 @@ EXTENSION = object-tree -DATA = sql/object-tree--0.36.0.sql +DATA = sql/object-tree--0.39.0.sql PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) diff --git a/packages/object-tree/__tests__/objects/__snapshots__/commits.test.ts.snap b/packages/object-tree/__tests__/objects/__snapshots__/commits.test.ts.snap index 0fc5467b0..63bdd26d8 100644 --- a/packages/object-tree/__tests__/objects/__snapshots__/commits.test.ts.snap +++ b/packages/object-tree/__tests__/objects/__snapshots__/commits.test.ts.snap @@ -3,7 +3,7 @@ exports[`commits create a ref 1`] = ` [ { - "set_and_commit": "1e6e8275-58c4-5d05-b79a-6d84d119dbfa", + "tree_hash": "1e6e8275-58c4-5d05-b79a-6d84d119dbfa", }, ] `; diff --git a/packages/object-tree/__tests__/objects/__snapshots__/repos.test.ts.snap b/packages/object-tree/__tests__/objects/__snapshots__/repos.test.ts.snap index fe5db986d..60b17b4ec 100644 --- a/packages/object-tree/__tests__/objects/__snapshots__/repos.test.ts.snap +++ b/packages/object-tree/__tests__/objects/__snapshots__/repos.test.ts.snap @@ -3,7 +3,7 @@ exports[`create a ref 1`] = ` [ { - "set_and_commit": "9f405c5f-8a48-5de8-afdb-1b7e9cc1ea40", + "tree_hash": "9f405c5f-8a48-5de8-afdb-1b7e9cc1ea40", }, ] `; diff --git a/packages/object-tree/__tests__/objects/commits.test.ts b/packages/object-tree/__tests__/objects/commits.test.ts index 669faf9e2..0c3792616 100644 --- a/packages/object-tree/__tests__/objects/commits.test.ts +++ b/packages/object-tree/__tests__/objects/commits.test.ts @@ -60,7 +60,7 @@ describe('commits', () => { ); const res = await pg.any( - `SELECT * FROM object_tree_public.set_and_commit( + `SELECT (object_tree_public.set_and_commit( s_id := $1::uuid, store_id := $2::uuid, refname := $3::text, @@ -68,7 +68,7 @@ describe('commits', () => { data := $5::jsonb, kids := $6::uuid[], ktree := $7::text[] - )`, + )).tree_id AS tree_hash`, [scope_id, store_id, 'master', ['a', 'b', 'c.yaml'], { content: 'type: hi' }, [], []] ); expect(snapshot(res)).toMatchSnapshot(); diff --git a/packages/object-tree/__tests__/objects/repos.test.ts b/packages/object-tree/__tests__/objects/repos.test.ts index aec3d6def..c10aad192 100644 --- a/packages/object-tree/__tests__/objects/repos.test.ts +++ b/packages/object-tree/__tests__/objects/repos.test.ts @@ -35,7 +35,7 @@ it('create a ref', async () => { ]); const res = await pg.any( - `SELECT * FROM object_tree_public.set_and_commit( + `SELECT (object_tree_public.set_and_commit( s_id := $1::uuid, store_id := $2::uuid, refname := $3::text, @@ -43,7 +43,7 @@ it('create a ref', async () => { data := $5::jsonb, kids := $6::uuid[], ktree := $7::text[] - )`, + )).tree_id AS tree_hash`, [ scope_id, store_id, diff --git a/packages/object-tree/__tests__/objects/set-many-and-commit.test.ts b/packages/object-tree/__tests__/objects/set-many-and-commit.test.ts new file mode 100644 index 000000000..9d1752982 --- /dev/null +++ b/packages/object-tree/__tests__/objects/set-many-and-commit.test.ts @@ -0,0 +1,153 @@ +jest.setTimeout(60000); + +import { getConnections, PgTestClient } from 'pgsql-test'; + +// One scope per repo: an empty root is a content hash like any other node, so +// two empty repos in one scope would collide on the primary key. +const eager_scope = 'd0f7ab73-356f-4aac-b9cb-d1a4274906d6'; +const batched_scope = '7c2f8a10-4d3b-4a5e-9c11-2f6b8d0a4e77'; + +let pg: PgTestClient; +let teardown: () => Promise; + +beforeAll(async () => { + ({ pg, teardown } = await getConnections()); +}); + +afterAll(async () => { + try { + await teardown(); + } catch (e) { + // ignore + } +}); + +beforeEach(async () => { + await pg.beforeEach(); +}); + +afterEach(async () => { + await pg.afterEach(); +}); + +interface Entry { + path: string[]; + data: any; +} + +const initRepo = async (scope_id: string, store_id: string) => { + await pg.any( + `SELECT object_tree_public.init_empty_repo( + s_id := $1::uuid, store_id := $2::uuid + )`, + [scope_id, store_id] + ); +}; + +const setAndCommit = async (scope_id: string, store_id: string, entry: Entry) => { + const [row] = await pg.any( + `SELECT (object_tree_public.set_and_commit( + s_id := $1::uuid, + store_id := $2::uuid, + refname := 'main', + path := $3::text[], + data := $4::jsonb, + kids := '{}'::uuid[], + ktree := '{}'::text[] + )).tree_id AS tree_id`, + [scope_id, store_id, entry.path, entry.data] + ); + return row.tree_id as string; +}; + +const setManyAndCommit = async (scope_id: string, store_id: string, entries: Entry[]) => { + const [row] = await pg.any( + `SELECT (object_tree_public.set_many_and_commit( + s_id := $1::uuid, + store_id := $2::uuid, + refname := 'main', + entries := $3::jsonb + )).tree_id AS tree_id`, + [ + scope_id, + store_id, + JSON.stringify( + entries.map((e) => ({ path: e.path, data: e.data, kids: [] as string[], ktree: [] as string[] })) + ) + ] + ); + return row.tree_id as string; +}; + +const countCommits = async (scope_id: string, store_id: string) => { + const [row] = await pg.any( + `SELECT count(*)::int AS n FROM object_tree_public.commit + WHERE scope_id = $1 AND store_id = $2`, + [scope_id, store_id] + ); + return row.n as number; +}; + +const getObject = async (scope_id: string, store_id: string, path: string[]) => { + const [row] = await pg.any( + `SELECT * FROM object_tree_public.get_object_at_path( + s_id := $1::uuid, store_id := $2::uuid, path := $3::text[], refname := 'main' + )`, + [scope_id, store_id, path] + ); + return row; +}; + +const entries: Entry[] = [ + { path: ['sql', 'deploy', 'a.sql'], data: { body: 'a' } }, + { path: ['sql', 'deploy', 'b.sql'], data: { body: 'b' } }, + { path: ['sql', 'revert', 'a.sql'], data: { body: 'revert a' } }, + { path: ['package.json'], data: { name: 'pkg' } } +]; + +describe('set_many_and_commit', () => { + it('one call is one commit and matches writing the entries one at a time', async () => { + const eager_store = '11111111-1111-4111-8111-111111111111'; + const batched_store = '22222222-2222-4222-8222-222222222222'; + await initRepo(eager_scope, eager_store); + await initRepo(batched_scope, batched_store); + + let eager_tree = ''; + for (const entry of entries) { + eager_tree = await setAndCommit(eager_scope, eager_store, entry); + } + const batched_tree = await setManyAndCommit(batched_scope, batched_store, entries); + + expect(batched_tree).toEqual(eager_tree); + expect(await countCommits(eager_scope, eager_store)).toEqual(1 + entries.length); + expect(await countCommits(batched_scope, batched_store)).toEqual(2); + expect((await getObject(batched_scope, batched_store, ['sql', 'deploy', 'b.sql'])).data).toEqual({ + body: 'b' + }); + }); + + it('a second batch merges with what the first batch wrote', async () => { + const store_id = '33333333-3333-4333-8333-333333333333'; + await initRepo(batched_scope, store_id); + await setManyAndCommit(batched_scope, store_id, entries); + await setManyAndCommit(batched_scope, store_id, [ + { path: ['sql', 'deploy', 'c.sql'], data: { body: 'c' } } + ]); + + expect((await getObject(batched_scope, store_id, ['sql', 'deploy', 'a.sql'])).data).toEqual({ + body: 'a' + }); + expect((await getObject(batched_scope, store_id, ['sql', 'deploy', 'c.sql'])).data).toEqual({ + body: 'c' + }); + expect(await countCommits(batched_scope, store_id)).toEqual(3); + }); + + it('nothing to write leaves the ref alone', async () => { + const store_id = '44444444-4444-4444-8444-444444444444'; + await initRepo(batched_scope, store_id); + const tree_id = await setManyAndCommit(batched_scope, store_id, entries); + expect(await setManyAndCommit(batched_scope, store_id, [])).toEqual(tree_id); + expect(await countCommits(batched_scope, store_id)).toEqual(2); + }); +}); diff --git a/packages/object-tree/deploy/schemas/object_tree_public/procedures/set_and_commit.sql b/packages/object-tree/deploy/schemas/object_tree_public/procedures/set_and_commit.sql index b4bc78afa..9107b73dc 100644 --- a/packages/object-tree/deploy/schemas/object_tree_public/procedures/set_and_commit.sql +++ b/packages/object-tree/deploy/schemas/object_tree_public/procedures/set_and_commit.sql @@ -6,6 +6,8 @@ BEGIN; +-- One-entry wrapper over object_tree_public.set_many_and_commit, which is +-- created later in the plan and resolved at call time. CREATE FUNCTION object_tree_public.set_and_commit( s_id uuid, store_id uuid, @@ -13,76 +15,48 @@ CREATE FUNCTION object_tree_public.set_and_commit( path text[], data jsonb, kids uuid[], - ktree text[] -) returns uuid as $$ -DECLARE - hash uuid; - - ref object_tree_public.ref; - com object_tree_public.commit; - + ktree text[], + message text DEFAULT NULL +) returns object_tree_public.commit as $$ BEGIN -SELECT * FROM - object_tree_public.ref r - WHERE r.scope_id = s_id - AND r.store_id = set_and_commit.store_id - AND r.name = refname -INTO ref; - -IF (NOT FOUND) THEN - RAISE EXCEPTION 'REF_NOT_FOUND'; -END IF; - -SELECT * FROM - object_tree_public.commit c - WHERE c.scope_id = s_id - AND c.store_id = set_and_commit.store_id - AND c.id = ref.commit_id -INTO com; - -IF (NOT FOUND) THEN - RAISE EXCEPTION 'COMMIT_NOT_FOUND'; -END IF; - -SELECT * FROM - object_store_public.insert_node_at_path - ( - s_id := s_id, - root := com.tree_id, - path := set_and_commit.path, - data := set_and_commit.data, - kids := set_and_commit.kids, - ktree := set_and_commit.ktree +RETURN object_tree_public.set_many_and_commit( + s_id := s_id, + store_id := set_and_commit.store_id, + refname := set_and_commit.refname, + message := set_and_commit.message, + entries := jsonb_build_array( + jsonb_build_object( + 'path', coalesce(to_jsonb(set_and_commit.path), '[]'::jsonb), + 'kids', to_jsonb(set_and_commit.kids), + 'ktree', to_jsonb(set_and_commit.ktree) + ) || + -- an absent key means "no data", which is not the same node as one whose + -- data is the json value null + CASE WHEN set_and_commit.data IS NULL THEN + '{}'::jsonb + ELSE + jsonb_build_object('data', set_and_commit.data) + END ) -INTO hash; - -INSERT INTO object_tree_public.commit ( - scope_id, - store_id, - message, - parent_ids, - tree_id -) VALUES (s_id, set_and_commit.store_id, NOW(), ARRAY[com.id]::uuid[], hash) -RETURNING * INTO com; - -UPDATE object_tree_public.ref r - SET commit_id = com.id -WHERE r.id = ref.id; - -RETURN hash; +); END; $$ LANGUAGE 'plpgsql' VOLATILE; +-- Writes one node's data while keeping the children it already has, then +-- commits. Not expressible through set_many_and_commit: the batched primitive +-- takes a node's children as given (an absent kids/ktree means "no children"), +-- whereas this reads the existing node to carry them over. CREATE FUNCTION object_tree_public.set_props_and_commit( s_id uuid, store_id uuid, refname text, path text[], - data jsonb -) returns uuid as $$ + data jsonb, + message text DEFAULT NULL +) returns object_tree_public.commit as $$ DECLARE hash uuid; @@ -124,14 +98,14 @@ INSERT INTO object_tree_public.commit ( message, parent_ids, tree_id -) VALUES (s_id, set_props_and_commit.store_id, NOW(), ARRAY[com.id]::uuid[], hash) +) VALUES (s_id, set_props_and_commit.store_id, set_props_and_commit.message, ARRAY[com.id]::uuid[], hash) RETURNING * INTO com; UPDATE object_tree_public.ref r SET commit_id = com.id WHERE r.id = ref.id; -RETURN hash; +RETURN com; END; $$ LANGUAGE 'plpgsql' VOLATILE; diff --git a/packages/object-tree/deploy/schemas/object_tree_public/procedures/set_many_and_commit.sql b/packages/object-tree/deploy/schemas/object_tree_public/procedures/set_many_and_commit.sql new file mode 100644 index 000000000..8301dc49f --- /dev/null +++ b/packages/object-tree/deploy/schemas/object_tree_public/procedures/set_many_and_commit.sql @@ -0,0 +1,87 @@ +-- Deploy schemas/object_tree_public/procedures/set_many_and_commit to pg + +-- requires: schemas/object_tree_public/schema +-- requires: schemas/object_tree_public/tables/commit/table +-- requires: schemas/object_tree_public/tables/ref/table + +BEGIN; + +-- Apply a whole set of writes to a ref and record them as one commit. +-- +-- entries is [{ "path": ["a","b"], "data": {...} }, ...]; an entry may also +-- carry "kids" and "ktree" to write a node's children explicitly. Entries are +-- applied in order, so a repeated path is last-write-wins, and the ref moves +-- once: one call is one commit, not one commit per write. Nothing to write +-- leaves the ref untouched and returns the commit the ref already points at. +-- +-- Returns the commit row rather than the new tree id: a caller that only wants +-- the tree reads .tree_id, and a caller stamping provenance (every generated +-- merkle writer does) gets .id without re-reading the ref. This is the same +-- shape the generated stores' {prefix}set_many_and_commit returns. +CREATE FUNCTION object_tree_public.set_many_and_commit (s_id uuid, store_id uuid, refname text, entries jsonb, message text DEFAULT NULL) + RETURNS object_tree_public.commit + AS $$ +DECLARE + hash uuid; + paths jsonb; + datas jsonb[]; + kids_list jsonb; + ktree_list jsonb; + ref object_tree_public.ref; + com object_tree_public.commit; +BEGIN + SELECT + * + FROM + object_tree_public.ref AS r + WHERE + r.scope_id = s_id + AND r.store_id = set_many_and_commit.store_id + AND r.name = refname INTO ref; + IF (NOT FOUND) THEN + RAISE EXCEPTION 'REF_NOT_FOUND'; + END IF; + SELECT + * + FROM + object_tree_public.commit AS c + WHERE + c.scope_id = s_id + AND c.store_id = set_many_and_commit.store_id + AND c.id = ref.commit_id INTO com; + IF (NOT FOUND) THEN + RAISE EXCEPTION 'COMMIT_NOT_FOUND'; + END IF; + IF (entries IS NULL OR jsonb_array_length(entries) = 0) THEN + RETURN com; + END IF; + SELECT + jsonb_agg(e.value -> 'path' ORDER BY e.ord), + array_agg(e.value -> 'data' ORDER BY e.ord), + jsonb_agg(coalesce(e.value -> 'kids', 'null'::jsonb) ORDER BY e.ord), + jsonb_agg(coalesce(e.value -> 'ktree', 'null'::jsonb) ORDER BY e.ord) + FROM jsonb_array_elements(set_many_and_commit.entries) WITH ORDINALITY AS e (value, ord) INTO paths, + datas, + kids_list, + ktree_list; + SELECT + * + FROM + object_store_public.insert_nodes_at_paths (s_id := s_id, root := com.tree_id, paths := paths, datas := datas, kids_list := kids_list, ktree_list := ktree_list) INTO hash; + INSERT INTO object_tree_public.commit (scope_id, store_id, message, parent_ids, tree_id) + VALUES (s_id, set_many_and_commit.store_id, set_many_and_commit.message, ARRAY[com.id]::uuid[], hash) + RETURNING + * INTO com; + UPDATE + object_tree_public.ref AS r + SET + commit_id = com.id + WHERE + r.id = ref.id; + RETURN com; +END; +$$ +LANGUAGE plpgsql +VOLATILE; + +COMMIT; diff --git a/packages/object-tree/object-tree.control b/packages/object-tree/object-tree.control index c48de7809..d4e47c891 100644 --- a/packages/object-tree/object-tree.control +++ b/packages/object-tree/object-tree.control @@ -1,6 +1,6 @@ # object-tree extension comment = 'object-tree extension - git-like version control for database objects' -default_version = '0.36.0' +default_version = '0.39.0' module_pathname = '$libdir/object-tree' requires = 'plpgsql,pgcrypto,object-store,pgpm-verify' relocatable = false diff --git a/packages/object-tree/package.json b/packages/object-tree/package.json index 9f5c59656..485ecaf83 100644 --- a/packages/object-tree/package.json +++ b/packages/object-tree/package.json @@ -1,6 +1,6 @@ { "name": "@pgpm/object-tree", - "version": "0.36.0", + "version": "0.39.0", "description": "Git-like version control for database objects with commits and refs", "author": "Dan Lynch ", "contributors": [ diff --git a/packages/object-tree/pgpm.plan b/packages/object-tree/pgpm.plan index 51bb58c36..a23e4d960 100644 --- a/packages/object-tree/pgpm.plan +++ b/packages/object-tree/pgpm.plan @@ -12,3 +12,4 @@ schemas/object_tree_public/procedures/init_empty_repo [schemas/object_tree_publi schemas/object_tree_public/procedures/set_and_commit [schemas/object_tree_public/schema schemas/object_tree_public/tables/commit/table schemas/object_tree_public/tables/ref/table] 2017-08-11T08:11:51Z skitch # add schemas/object_tree_public/procedures/set_and_commit schemas/object_tree_public/tables/ref/indexes/idx_unique_object_ref_name [schemas/object_tree_public/schema schemas/object_tree_public/tables/ref/table] 2017-08-11T08:11:51Z skitch # add schemas/object_tree_public/tables/ref/indexes/idx_unique_object_ref_name schemas/object_tree_public/tables/store/table [schemas/object_tree_public/schema] 2026-01-06T12:00:00Z devin # add store table for multi-repo support +schemas/object_tree_public/procedures/set_many_and_commit [schemas/object_tree_public/schema schemas/object_tree_public/tables/commit/table schemas/object_tree_public/tables/ref/table] 2026-08-06T00:00:00Z devin # add schemas/object_tree_public/procedures/set_many_and_commit diff --git a/packages/object-tree/revert/schemas/object_tree_public/procedures/get_object_at_path.sql b/packages/object-tree/revert/schemas/object_tree_public/procedures/get_object_at_path.sql index 103101d7e..e7229307e 100644 --- a/packages/object-tree/revert/schemas/object_tree_public/procedures/get_object_at_path.sql +++ b/packages/object-tree/revert/schemas/object_tree_public/procedures/get_object_at_path.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION object_tree_public.get_object_at_path; +DROP FUNCTION object_tree_public.get_object_at_path(uuid, uuid, text[], text); COMMIT; diff --git a/packages/object-tree/revert/schemas/object_tree_public/procedures/init_empty_repo.sql b/packages/object-tree/revert/schemas/object_tree_public/procedures/init_empty_repo.sql index 2ead050cf..a50ca27aa 100644 --- a/packages/object-tree/revert/schemas/object_tree_public/procedures/init_empty_repo.sql +++ b/packages/object-tree/revert/schemas/object_tree_public/procedures/init_empty_repo.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION object_tree_public.init_empty_repo; +DROP FUNCTION object_tree_public.init_empty_repo(uuid, uuid); COMMIT; diff --git a/packages/object-tree/revert/schemas/object_tree_public/procedures/rev_parse.sql b/packages/object-tree/revert/schemas/object_tree_public/procedures/rev_parse.sql index 5c9a07f2e..91aad502a 100644 --- a/packages/object-tree/revert/schemas/object_tree_public/procedures/rev_parse.sql +++ b/packages/object-tree/revert/schemas/object_tree_public/procedures/rev_parse.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION object_tree_public.rev_parse; +DROP FUNCTION object_tree_public.rev_parse(uuid, uuid, text); COMMIT; diff --git a/packages/object-tree/revert/schemas/object_tree_public/procedures/set_and_commit.sql b/packages/object-tree/revert/schemas/object_tree_public/procedures/set_and_commit.sql index c3f1724b2..5e0815a5e 100644 --- a/packages/object-tree/revert/schemas/object_tree_public/procedures/set_and_commit.sql +++ b/packages/object-tree/revert/schemas/object_tree_public/procedures/set_and_commit.sql @@ -2,7 +2,7 @@ BEGIN; -DROP FUNCTION object_tree_public.set_props_and_commit; -DROP FUNCTION object_tree_public.set_and_commit; +DROP FUNCTION object_tree_public.set_props_and_commit(uuid, uuid, text, text[], jsonb, text); +DROP FUNCTION object_tree_public.set_and_commit(uuid, uuid, text, text[], jsonb, uuid[], text[], text); COMMIT; diff --git a/packages/object-tree/revert/schemas/object_tree_public/procedures/set_many_and_commit.sql b/packages/object-tree/revert/schemas/object_tree_public/procedures/set_many_and_commit.sql new file mode 100644 index 000000000..4c03bcb8d --- /dev/null +++ b/packages/object-tree/revert/schemas/object_tree_public/procedures/set_many_and_commit.sql @@ -0,0 +1,7 @@ +-- Revert schemas/object_tree_public/procedures/set_many_and_commit from pg + +BEGIN; + +DROP FUNCTION object_tree_public.set_many_and_commit(uuid, uuid, text, jsonb, text); + +COMMIT; diff --git a/packages/object-tree/sql/object-tree--0.36.0.bundle.tar.gz b/packages/object-tree/sql/object-tree--0.36.0.bundle.tar.gz deleted file mode 100644 index 69365a71f..000000000 Binary files a/packages/object-tree/sql/object-tree--0.36.0.bundle.tar.gz and /dev/null differ diff --git a/packages/object-tree/sql/object-tree--0.39.0.bundle.tar.gz b/packages/object-tree/sql/object-tree--0.39.0.bundle.tar.gz new file mode 100644 index 000000000..8e55ab932 Binary files /dev/null and b/packages/object-tree/sql/object-tree--0.39.0.bundle.tar.gz differ diff --git a/packages/object-tree/sql/object-tree--0.36.0.sql b/packages/object-tree/sql/object-tree--0.39.0.sql similarity index 69% rename from packages/object-tree/sql/object-tree--0.36.0.sql rename to packages/object-tree/sql/object-tree--0.39.0.sql index 0b27cdac3..1cf896f34 100644 --- a/packages/object-tree/sql/object-tree--0.36.0.sql +++ b/packages/object-tree/sql/object-tree--0.39.0.sql @@ -163,64 +163,31 @@ CREATE FUNCTION object_tree_public.set_and_commit( path text[], data jsonb, kids uuid[], - ktree text[] -) RETURNS uuid AS $EOFCODE$ -DECLARE - hash uuid; - - ref object_tree_public.ref; - com object_tree_public.commit; - + ktree text[], + message text DEFAULT NULL +) RETURNS object_tree_public.commit AS $EOFCODE$ BEGIN -SELECT * FROM - object_tree_public.ref r - WHERE r.scope_id = s_id - AND r.store_id = set_and_commit.store_id - AND r.name = refname -INTO ref; - -IF (NOT FOUND) THEN - RAISE EXCEPTION 'REF_NOT_FOUND'; -END IF; - -SELECT * FROM - object_tree_public.commit c - WHERE c.scope_id = s_id - AND c.store_id = set_and_commit.store_id - AND c.id = ref.commit_id -INTO com; - -IF (NOT FOUND) THEN - RAISE EXCEPTION 'COMMIT_NOT_FOUND'; -END IF; - -SELECT * FROM - object_store_public.insert_node_at_path - ( - s_id := s_id, - root := com.tree_id, - path := set_and_commit.path, - data := set_and_commit.data, - kids := set_and_commit.kids, - ktree := set_and_commit.ktree +RETURN object_tree_public.set_many_and_commit( + s_id := s_id, + store_id := set_and_commit.store_id, + refname := set_and_commit.refname, + message := set_and_commit.message, + entries := jsonb_build_array( + jsonb_build_object( + 'path', coalesce(to_jsonb(set_and_commit.path), '[]'::jsonb), + 'kids', to_jsonb(set_and_commit.kids), + 'ktree', to_jsonb(set_and_commit.ktree) + ) || + -- an absent key means "no data", which is not the same node as one whose + -- data is the json value null + CASE WHEN set_and_commit.data IS NULL THEN + '{}'::jsonb + ELSE + jsonb_build_object('data', set_and_commit.data) + END ) -INTO hash; - -INSERT INTO object_tree_public.commit ( - scope_id, - store_id, - message, - parent_ids, - tree_id -) VALUES (s_id, set_and_commit.store_id, NOW(), ARRAY[com.id]::uuid[], hash) -RETURNING * INTO com; - -UPDATE object_tree_public.ref r - SET commit_id = com.id -WHERE r.id = ref.id; - -RETURN hash; +); END; $EOFCODE$ LANGUAGE plpgsql VOLATILE; @@ -229,8 +196,9 @@ CREATE FUNCTION object_tree_public.set_props_and_commit( store_id uuid, refname text, path text[], - data jsonb -) RETURNS uuid AS $EOFCODE$ + data jsonb, + message text DEFAULT NULL +) RETURNS object_tree_public.commit AS $EOFCODE$ DECLARE hash uuid; @@ -272,14 +240,14 @@ INSERT INTO object_tree_public.commit ( message, parent_ids, tree_id -) VALUES (s_id, set_props_and_commit.store_id, NOW(), ARRAY[com.id]::uuid[], hash) +) VALUES (s_id, set_props_and_commit.store_id, set_props_and_commit.message, ARRAY[com.id]::uuid[], hash) RETURNING * INTO com; UPDATE object_tree_public.ref r SET commit_id = com.id WHERE r.id = ref.id; -RETURN hash; +RETURN com; END; $EOFCODE$ LANGUAGE plpgsql VOLATILE; @@ -304,4 +272,72 @@ COMMENT ON COLUMN object_tree_public.store.scope_id IS 'The scope this store bel COMMENT ON COLUMN object_tree_public.store.hash IS 'The current head tree_id for this store.'; -CREATE UNIQUE INDEX idx_unique_store_name ON object_tree_public.store (scope_id, (decode(md5(lower(name)), 'hex'))); \ No newline at end of file +CREATE UNIQUE INDEX idx_unique_store_name ON object_tree_public.store (scope_id, (decode(md5(lower(name)), 'hex'))); + +CREATE FUNCTION object_tree_public.set_many_and_commit( + s_id uuid, + store_id uuid, + refname text, + entries jsonb, + message text DEFAULT NULL +) RETURNS object_tree_public.commit AS $EOFCODE$ +DECLARE + hash uuid; + paths jsonb; + datas jsonb[]; + kids_list jsonb; + ktree_list jsonb; + ref object_tree_public.ref; + com object_tree_public.commit; +BEGIN + SELECT + * + FROM + object_tree_public.ref AS r + WHERE + r.scope_id = s_id + AND r.store_id = set_many_and_commit.store_id + AND r.name = refname INTO ref; + IF (NOT FOUND) THEN + RAISE EXCEPTION 'REF_NOT_FOUND'; + END IF; + SELECT + * + FROM + object_tree_public.commit AS c + WHERE + c.scope_id = s_id + AND c.store_id = set_many_and_commit.store_id + AND c.id = ref.commit_id INTO com; + IF (NOT FOUND) THEN + RAISE EXCEPTION 'COMMIT_NOT_FOUND'; + END IF; + IF (entries IS NULL OR jsonb_array_length(entries) = 0) THEN + RETURN com; + END IF; + SELECT + jsonb_agg(e.value -> 'path' ORDER BY e.ord), + array_agg(e.value -> 'data' ORDER BY e.ord), + jsonb_agg(coalesce(e.value -> 'kids', 'null'::jsonb) ORDER BY e.ord), + jsonb_agg(coalesce(e.value -> 'ktree', 'null'::jsonb) ORDER BY e.ord) + FROM jsonb_array_elements(set_many_and_commit.entries) WITH ORDINALITY AS e (value, ord) INTO paths, + datas, + kids_list, + ktree_list; + SELECT + * + FROM + object_store_public.insert_nodes_at_paths (s_id := s_id, root := com.tree_id, paths := paths, datas := datas, kids_list := kids_list, ktree_list := ktree_list) INTO hash; + INSERT INTO object_tree_public.commit (scope_id, store_id, message, parent_ids, tree_id) + VALUES (s_id, set_many_and_commit.store_id, set_many_and_commit.message, ARRAY[com.id]::uuid[], hash) + RETURNING + * INTO com; + UPDATE + object_tree_public.ref AS r + SET + commit_id = com.id + WHERE + r.id = ref.id; + RETURN com; +END; +$EOFCODE$ LANGUAGE plpgsql VOLATILE; \ No newline at end of file diff --git a/packages/object-tree/verify/schemas/object_tree_private/schema.sql b/packages/object-tree/verify/schemas/object_tree_private/schema.sql index 78e61433b..3b9c12f4c 100644 --- a/packages/object-tree/verify/schemas/object_tree_private/schema.sql +++ b/packages/object-tree/verify/schemas/object_tree_private/schema.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_schema ('object_tree_private'); +SELECT assert_schema('object_tree_private'::regnamespace); ROLLBACK; diff --git a/packages/object-tree/verify/schemas/object_tree_public/procedures/get_object_at_path.sql b/packages/object-tree/verify/schemas/object_tree_public/procedures/get_object_at_path.sql index ebde69228..97e048e5e 100644 --- a/packages/object-tree/verify/schemas/object_tree_public/procedures/get_object_at_path.sql +++ b/packages/object-tree/verify/schemas/object_tree_public/procedures/get_object_at_path.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('object_tree_public.get_object_at_path'); +SELECT assert_function('object_tree_public.get_object_at_path(uuid, uuid, text[], text)'::regprocedure); ROLLBACK; diff --git a/packages/object-tree/verify/schemas/object_tree_public/procedures/init_empty_repo.sql b/packages/object-tree/verify/schemas/object_tree_public/procedures/init_empty_repo.sql index 5e3fb81b4..7a2e0cba1 100644 --- a/packages/object-tree/verify/schemas/object_tree_public/procedures/init_empty_repo.sql +++ b/packages/object-tree/verify/schemas/object_tree_public/procedures/init_empty_repo.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('object_tree_public.init_empty_repo'); +SELECT assert_function('object_tree_public.init_empty_repo(uuid, uuid)'::regprocedure); ROLLBACK; diff --git a/packages/object-tree/verify/schemas/object_tree_public/procedures/rev_parse.sql b/packages/object-tree/verify/schemas/object_tree_public/procedures/rev_parse.sql index 0be73fed5..e63ff0973 100644 --- a/packages/object-tree/verify/schemas/object_tree_public/procedures/rev_parse.sql +++ b/packages/object-tree/verify/schemas/object_tree_public/procedures/rev_parse.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('object_tree_public.rev_parse'); +SELECT assert_function('object_tree_public.rev_parse(uuid, uuid, text)'::regprocedure); ROLLBACK; diff --git a/packages/object-tree/verify/schemas/object_tree_public/procedures/set_and_commit.sql b/packages/object-tree/verify/schemas/object_tree_public/procedures/set_and_commit.sql index 485a57a7f..842b074b0 100644 --- a/packages/object-tree/verify/schemas/object_tree_public/procedures/set_and_commit.sql +++ b/packages/object-tree/verify/schemas/object_tree_public/procedures/set_and_commit.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('object_tree_public.set_and_commit'); +SELECT assert_function('object_tree_public.set_and_commit(uuid, uuid, text, text[], jsonb, uuid[], text[], text)'::regprocedure); ROLLBACK; diff --git a/packages/object-tree/verify/schemas/object_tree_public/procedures/set_many_and_commit.sql b/packages/object-tree/verify/schemas/object_tree_public/procedures/set_many_and_commit.sql new file mode 100644 index 000000000..b7eecf403 --- /dev/null +++ b/packages/object-tree/verify/schemas/object_tree_public/procedures/set_many_and_commit.sql @@ -0,0 +1,7 @@ +-- Verify schemas/object_tree_public/procedures/set_many_and_commit on pg + +BEGIN; + +SELECT assert_function('object_tree_public.set_many_and_commit(uuid, uuid, text, jsonb, text)'::regprocedure); + +ROLLBACK; diff --git a/packages/object-tree/verify/schemas/object_tree_public/schema.sql b/packages/object-tree/verify/schemas/object_tree_public/schema.sql index de46f16d1..8ff357aff 100644 --- a/packages/object-tree/verify/schemas/object_tree_public/schema.sql +++ b/packages/object-tree/verify/schemas/object_tree_public/schema.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_schema ('object_tree_public'); +SELECT assert_schema('object_tree_public'::regnamespace); ROLLBACK; diff --git a/packages/object-tree/verify/schemas/object_tree_public/tables/commit/table.sql b/packages/object-tree/verify/schemas/object_tree_public/tables/commit/table.sql index 72b40ce76..2af05557d 100644 --- a/packages/object-tree/verify/schemas/object_tree_public/tables/commit/table.sql +++ b/packages/object-tree/verify/schemas/object_tree_public/tables/commit/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('object_tree_public.commit'); +SELECT assert_table('object_tree_public.commit'::regclass); ROLLBACK; diff --git a/packages/object-tree/verify/schemas/object_tree_public/tables/ref/indexes/idx_unique_object_ref_name.sql b/packages/object-tree/verify/schemas/object_tree_public/tables/ref/indexes/idx_unique_object_ref_name.sql index 28c431a3b..c672fab7b 100644 --- a/packages/object-tree/verify/schemas/object_tree_public/tables/ref/indexes/idx_unique_object_ref_name.sql +++ b/packages/object-tree/verify/schemas/object_tree_public/tables/ref/indexes/idx_unique_object_ref_name.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_index ('object_tree_public.ref', 'idx_unique_object_ref_name'); +SELECT assert_index('object_tree_public.idx_unique_object_ref_name'::regclass, 'object_tree_public.ref'::regclass, true); ROLLBACK; diff --git a/packages/object-tree/verify/schemas/object_tree_public/tables/ref/table.sql b/packages/object-tree/verify/schemas/object_tree_public/tables/ref/table.sql index 7e3f54373..979d2c44f 100644 --- a/packages/object-tree/verify/schemas/object_tree_public/tables/ref/table.sql +++ b/packages/object-tree/verify/schemas/object_tree_public/tables/ref/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('object_tree_public.ref'); +SELECT assert_table('object_tree_public.ref'::regclass); ROLLBACK; diff --git a/packages/object-tree/verify/schemas/object_tree_public/tables/store/table.sql b/packages/object-tree/verify/schemas/object_tree_public/tables/store/table.sql index 7b8e5d058..5861f04e4 100644 --- a/packages/object-tree/verify/schemas/object_tree_public/tables/store/table.sql +++ b/packages/object-tree/verify/schemas/object_tree_public/tables/store/table.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_table ('object_tree_public.store'); +SELECT assert_table('object_tree_public.store'::regclass); ROLLBACK; diff --git a/packages/partman/Makefile b/packages/partman/Makefile index 89bc0886b..80e1d0939 100644 --- a/packages/partman/Makefile +++ b/packages/partman/Makefile @@ -1,5 +1,5 @@ EXTENSION = pgpm-partman -DATA = sql/pgpm-partman--0.36.0.sql +DATA = sql/pgpm-partman--0.39.0.sql PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) diff --git a/packages/partman/__tests__/unparent.test.ts b/packages/partman/__tests__/unparent.test.ts new file mode 100644 index 000000000..7578bf854 --- /dev/null +++ b/packages/partman/__tests__/unparent.test.ts @@ -0,0 +1,98 @@ +import { getConnections, PgTestClient } from 'pgsql-test'; + +let pg: PgTestClient; +let teardown: () => Promise; + +const database_id = '11111111-1111-4111-8111-111111111111'; +const schema_id = '22222222-2222-4222-8222-222222222222'; +const table_id = '33333333-3333-4333-8333-333333333333'; +const field_id = '44444444-4444-4444-8444-444444444444'; +const partition_id = '55555555-5555-4555-8555-555555555555'; + +beforeAll(async () => { + ({ pg, teardown } = await getConnections()); +}); + +afterAll(async () => { + await teardown(); +}); + +beforeEach(async () => { + await pg.beforeEach(); + + await pg.any(`CREATE SCHEMA partman_test_public`); + await pg.any(`CREATE TABLE partman_test_public.events ( + id uuid NOT NULL, + created_at timestamptz NOT NULL + ) PARTITION BY RANGE (created_at)`); + + await pg.any( + `INSERT INTO metaschema_public.database (id, name, schema_hash, hash) + VALUES ($1, 'partman_test', 'partman_test', gen_random_uuid())`, + [database_id] + ); + await pg.any( + `INSERT INTO metaschema_public.schema (id, database_id, name, schema_name) + VALUES ($1, $2, 'partman_test_public', 'partman_test_public')`, + [schema_id, database_id] + ); + await pg.any( + `INSERT INTO metaschema_public.table (id, database_id, schema_id, name) + VALUES ($1, $2, $3, 'events')`, + [table_id, database_id, schema_id] + ); + await pg.any( + `INSERT INTO metaschema_public.field (id, database_id, table_id, name, type) + VALUES ($1, $2, $3, 'created_at', '"timestamp"'::jsonb)`, + [field_id, database_id, table_id] + ); +}); + +afterEach(async () => { + await pg.afterEach(); +}); + +const parent = async () => + pg.any( + `INSERT INTO metaschema_public.partition + (id, database_id, table_id, strategy, partition_key_id, "interval", premake) + VALUES ($1, $2, $3, 'range', $4, '1 day', 2)`, + [partition_id, database_id, table_id, field_id] + ); + +const templateExists = async () => + ( + await pg.one( + `SELECT to_regclass('partman.template_partman_test_public_events') IS NOT NULL AS exists` + ) + ).exists; + +describe('metaschema partition rows', () => { + it('parents the table with pg_partman on insert', async () => { + await parent(); + + const config = await pg.one( + `SELECT parent_table, control FROM partman.part_config + WHERE parent_table = 'partman_test_public.events'` + ); + expect(config.control).toBe('created_at'); + expect(await templateExists()).toBe(true); + }); + + it('unparents the table on delete, leaving no template table behind', async () => { + await parent(); + + await pg.any(`DELETE FROM metaschema_public.partition WHERE id = $1`, [ + partition_id + ]); + + const configs = await pg.any( + `SELECT 1 FROM partman.part_config WHERE parent_table = 'partman_test_public.events'` + ); + expect(configs).toHaveLength(0); + + // The template table outliving the parent is what blocked DROP TYPE on the + // parent's column types during a revert. + expect(await templateExists()).toBe(false); + }); +}); diff --git a/packages/partman/deploy/schemas/partman/triggers/after_delete_partition.sql b/packages/partman/deploy/schemas/partman/triggers/after_delete_partition.sql new file mode 100644 index 000000000..de1cd61d2 --- /dev/null +++ b/packages/partman/deploy/schemas/partman/triggers/after_delete_partition.sql @@ -0,0 +1,55 @@ +-- Deploy schemas/partman/triggers/after_delete_partition to pg + +-- requires: schemas/partman/triggers/after_insert_partition +-- requires: metaschema-schema:schemas/metaschema_public/tables/partition/table +-- requires: metaschema-schema:schemas/metaschema_public/tables/table/table +-- requires: metaschema-schema:schemas/metaschema_public/tables/schema/table + +BEGIN; + +-- Inverse of tg_after_insert_partition: unparents the config from pg_partman. +-- create_parent leaves two objects behind that outlive the parent table's own +-- DROP ... CASCADE — the part_config row and the partman.template_ +-- table, whose columns hold the parent's types and so block dropping them. +CREATE FUNCTION partman.tg_after_delete_partition() +RETURNS TRIGGER AS $$ +DECLARE + v_parent_table text; + v_template_table text; +BEGIN + SELECT s.schema_name || '.' || t.name + INTO v_parent_table + FROM metaschema_public.table t + JOIN metaschema_public.schema s + ON (s.id = t.schema_id AND s.database_id = t.database_id) + WHERE t.id = OLD.table_id; + + IF v_parent_table IS NULL THEN + RETURN OLD; + END IF; + + DELETE FROM partman.part_config + WHERE parent_table = v_parent_table + RETURNING template_table INTO v_template_table; + + IF v_template_table IS NOT NULL AND to_regclass(v_template_table) IS NOT NULL THEN + -- pgsql-lint-disable-next-line no-dynamic-sql -- DDL on a relation named by partman.part_config.template_table + EXECUTE format('DROP TABLE %s', to_regclass(v_template_table)::text); + END IF; + + RETURN OLD; +END; +$$ +LANGUAGE 'plpgsql' VOLATILE SECURITY DEFINER; + +-- SECURITY DEFINER justification: mirrors create_parent_with_retention. The +-- template table is owned by postgres (create_parent reassigns it), so the +-- authenticated role cannot drop it without SET ROLE. The function touches +-- only partman.part_config and the template table it names — never user data. + +CREATE TRIGGER partman_after_delete_partition + AFTER DELETE ON metaschema_public.partition + FOR EACH ROW + EXECUTE PROCEDURE partman.tg_after_delete_partition(); + +COMMIT; diff --git a/packages/partman/package.json b/packages/partman/package.json index d24cd8068..3929f5fe8 100644 --- a/packages/partman/package.json +++ b/packages/partman/package.json @@ -1,6 +1,6 @@ { "name": "@pgpm/partman", - "version": "0.36.0", + "version": "0.39.0", "description": "pg_partman wrapper - installs pg_partman into partman schema", "author": "Dan Lynch ", "contributors": [ diff --git a/packages/partman/pgpm-partman.control b/packages/partman/pgpm-partman.control index c7455dcf0..37ea0809b 100644 --- a/packages/partman/pgpm-partman.control +++ b/packages/partman/pgpm-partman.control @@ -1,6 +1,6 @@ # pgpm-partman extension comment = 'pg_partman wrapper - installs pg_partman into partman schema' -default_version = '0.36.0' +default_version = '0.39.0' module_pathname = '$libdir/pgpm-partman' requires = 'plpgsql,pg_partman,metaschema-schema' relocatable = false diff --git a/packages/partman/pgpm.plan b/packages/partman/pgpm.plan index 4beb29879..1a3fb72d7 100644 --- a/packages/partman/pgpm.plan +++ b/packages/partman/pgpm.plan @@ -9,3 +9,4 @@ schemas/partman/procedures/remove_parent_by_id 2026-05-16T00:00:01Z devin # metaschema-aware wrapper for partman verify schemas/partman/procedures/run_maintenance_by_id 2026-05-16T00:00:03Z devin # metaschema-aware wrapper for partman run_maintenance schemas/partman/triggers/after_insert_partition [schemas/partman/procedures/create_parent_with_retention metaschema-schema:schemas/metaschema_public/tables/partition/table metaschema-schema:schemas/metaschema_public/tables/table/table metaschema-schema:schemas/metaschema_public/tables/schema/table metaschema-schema:schemas/metaschema_public/tables/field/table] 2026-07-08T00:00:00Z devin # parent metaschema partition rows via pg_partman on consumer databases +schemas/partman/triggers/after_delete_partition [schemas/partman/triggers/after_insert_partition metaschema-schema:schemas/metaschema_public/tables/partition/table metaschema-schema:schemas/metaschema_public/tables/table/table metaschema-schema:schemas/metaschema_public/tables/schema/table] 2026-08-06T00:00:00Z devin # unparent metaschema partition rows from pg_partman on revert diff --git a/packages/partman/revert/schemas/partman/procedures/create_parent_by_id.sql b/packages/partman/revert/schemas/partman/procedures/create_parent_by_id.sql index fd2f9d580..aa12da56e 100644 --- a/packages/partman/revert/schemas/partman/procedures/create_parent_by_id.sql +++ b/packages/partman/revert/schemas/partman/procedures/create_parent_by_id.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION IF EXISTS partman.create_parent_by_id(uuid, text, text, text, int, text, boolean); +DROP FUNCTION partman.create_parent_by_id(uuid, text, text, text, int4, text, bool); COMMIT; diff --git a/packages/partman/revert/schemas/partman/procedures/create_parent_with_retention.sql b/packages/partman/revert/schemas/partman/procedures/create_parent_with_retention.sql index ee322e788..67d918ff1 100644 --- a/packages/partman/revert/schemas/partman/procedures/create_parent_with_retention.sql +++ b/packages/partman/revert/schemas/partman/procedures/create_parent_with_retention.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION IF EXISTS partman.create_parent_with_retention(text, text, text, text, int, text, boolean); +DROP FUNCTION partman.create_parent_with_retention(text, text, text, text, int4, text, bool); COMMIT; diff --git a/packages/partman/revert/schemas/partman/procedures/remove_parent_by_id.sql b/packages/partman/revert/schemas/partman/procedures/remove_parent_by_id.sql index 543ea06bd..7b0a6a2d8 100644 --- a/packages/partman/revert/schemas/partman/procedures/remove_parent_by_id.sql +++ b/packages/partman/revert/schemas/partman/procedures/remove_parent_by_id.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION IF EXISTS partman.remove_parent_by_id(uuid); +DROP FUNCTION partman.remove_parent_by_id(uuid); COMMIT; diff --git a/packages/partman/revert/schemas/partman/procedures/run_maintenance_by_id.sql b/packages/partman/revert/schemas/partman/procedures/run_maintenance_by_id.sql index 41d98ad23..6a2910b2d 100644 --- a/packages/partman/revert/schemas/partman/procedures/run_maintenance_by_id.sql +++ b/packages/partman/revert/schemas/partman/procedures/run_maintenance_by_id.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION IF EXISTS partman.run_maintenance_by_id(uuid, boolean); +DROP FUNCTION partman.run_maintenance_by_id(uuid, bool); COMMIT; diff --git a/packages/partman/revert/schemas/partman/procedures/verify_parent_by_id.sql b/packages/partman/revert/schemas/partman/procedures/verify_parent_by_id.sql index af250ee48..a6aab7b19 100644 --- a/packages/partman/revert/schemas/partman/procedures/verify_parent_by_id.sql +++ b/packages/partman/revert/schemas/partman/procedures/verify_parent_by_id.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION IF EXISTS partman.verify_parent_by_id(uuid); +DROP FUNCTION partman.verify_parent_by_id(uuid); COMMIT; diff --git a/packages/partman/revert/schemas/partman/triggers/after_delete_partition.sql b/packages/partman/revert/schemas/partman/triggers/after_delete_partition.sql new file mode 100644 index 000000000..90013cb4d --- /dev/null +++ b/packages/partman/revert/schemas/partman/triggers/after_delete_partition.sql @@ -0,0 +1,8 @@ +-- Revert schemas/partman/triggers/after_delete_partition from pg + +BEGIN; + +DROP TRIGGER partman_after_delete_partition ON metaschema_public.partition; +DROP FUNCTION partman.tg_after_delete_partition(); + +COMMIT; diff --git a/packages/partman/revert/schemas/partman/triggers/after_insert_partition.sql b/packages/partman/revert/schemas/partman/triggers/after_insert_partition.sql index 1402eb0c0..622407bb7 100644 --- a/packages/partman/revert/schemas/partman/triggers/after_insert_partition.sql +++ b/packages/partman/revert/schemas/partman/triggers/after_insert_partition.sql @@ -2,7 +2,7 @@ BEGIN; -DROP TRIGGER IF EXISTS partman_after_insert_partition ON metaschema_public.partition; -DROP FUNCTION IF EXISTS partman.tg_after_insert_partition(); +DROP TRIGGER partman_after_insert_partition ON metaschema_public.partition; +DROP FUNCTION partman.tg_after_insert_partition(); COMMIT; diff --git a/packages/partman/sql/pgpm-partman--0.36.0.bundle.tar.gz b/packages/partman/sql/pgpm-partman--0.36.0.bundle.tar.gz deleted file mode 100644 index a7a07d9be..000000000 Binary files a/packages/partman/sql/pgpm-partman--0.36.0.bundle.tar.gz and /dev/null differ diff --git a/packages/partman/sql/pgpm-partman--0.39.0.bundle.tar.gz b/packages/partman/sql/pgpm-partman--0.39.0.bundle.tar.gz new file mode 100644 index 000000000..0fa381c4d Binary files /dev/null and b/packages/partman/sql/pgpm-partman--0.39.0.bundle.tar.gz differ diff --git a/packages/partman/sql/pgpm-partman--0.36.0.sql b/packages/partman/sql/pgpm-partman--0.39.0.sql similarity index 83% rename from packages/partman/sql/pgpm-partman--0.36.0.sql rename to packages/partman/sql/pgpm-partman--0.39.0.sql index 1381b693c..c7ba16304 100644 --- a/packages/partman/sql/pgpm-partman--0.36.0.sql +++ b/packages/partman/sql/pgpm-partman--0.39.0.sql @@ -197,4 +197,39 @@ CREATE TRIGGER partman_after_insert_partition AFTER INSERT ON metaschema_public.partition FOR EACH ROW - EXECUTE PROCEDURE partman.tg_after_insert_partition(); \ No newline at end of file + EXECUTE PROCEDURE partman.tg_after_insert_partition(); + +CREATE FUNCTION partman.tg_after_delete_partition() RETURNS trigger AS $EOFCODE$ +DECLARE + v_parent_table text; + v_template_table text; +BEGIN + SELECT s.schema_name || '.' || t.name + INTO v_parent_table + FROM metaschema_public.table t + JOIN metaschema_public.schema s + ON (s.id = t.schema_id AND s.database_id = t.database_id) + WHERE t.id = OLD.table_id; + + IF v_parent_table IS NULL THEN + RETURN OLD; + END IF; + + DELETE FROM partman.part_config + WHERE parent_table = v_parent_table + RETURNING template_table INTO v_template_table; + + IF v_template_table IS NOT NULL AND to_regclass(v_template_table) IS NOT NULL THEN + -- pgsql-lint-disable-next-line no-dynamic-sql -- DDL on a relation named by partman.part_config.template_table + EXECUTE format('DROP TABLE %s', to_regclass(v_template_table)::text); + END IF; + + RETURN OLD; +END; +$EOFCODE$ LANGUAGE plpgsql VOLATILE SECURITY DEFINER; + +CREATE TRIGGER partman_after_delete_partition + AFTER DELETE + ON metaschema_public.partition + FOR EACH ROW + EXECUTE PROCEDURE partman.tg_after_delete_partition(); \ No newline at end of file diff --git a/packages/partman/verify/schemas/partman/procedures/create_parent_by_id.sql b/packages/partman/verify/schemas/partman/procedures/create_parent_by_id.sql index 6a605d104..29d1b38ec 100644 --- a/packages/partman/verify/schemas/partman/procedures/create_parent_by_id.sql +++ b/packages/partman/verify/schemas/partman/procedures/create_parent_by_id.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT has_function_privilege('partman.create_parent_by_id(uuid, text, text, text, int, text, boolean)', 'execute'); +SELECT assert_function('partman.create_parent_by_id(uuid, text, text, text, int4, text, bool)'::regprocedure); ROLLBACK; diff --git a/packages/partman/verify/schemas/partman/procedures/create_parent_with_retention.sql b/packages/partman/verify/schemas/partman/procedures/create_parent_with_retention.sql index e16d4fda4..321dfa557 100644 --- a/packages/partman/verify/schemas/partman/procedures/create_parent_with_retention.sql +++ b/packages/partman/verify/schemas/partman/procedures/create_parent_with_retention.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT has_function_privilege('partman.create_parent_with_retention(text, text, text, text, int, text, boolean)', 'execute'); +SELECT assert_function('partman.create_parent_with_retention(text, text, text, text, int4, text, bool)'::regprocedure); ROLLBACK; diff --git a/packages/partman/verify/schemas/partman/procedures/remove_parent_by_id.sql b/packages/partman/verify/schemas/partman/procedures/remove_parent_by_id.sql index 6a1dfe920..f7a4148db 100644 --- a/packages/partman/verify/schemas/partman/procedures/remove_parent_by_id.sql +++ b/packages/partman/verify/schemas/partman/procedures/remove_parent_by_id.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT has_function_privilege('partman.remove_parent_by_id(uuid)', 'execute'); +SELECT assert_function('partman.remove_parent_by_id(uuid)'::regprocedure); ROLLBACK; diff --git a/packages/partman/verify/schemas/partman/procedures/run_maintenance_by_id.sql b/packages/partman/verify/schemas/partman/procedures/run_maintenance_by_id.sql index 4ab17e31e..896c660f3 100644 --- a/packages/partman/verify/schemas/partman/procedures/run_maintenance_by_id.sql +++ b/packages/partman/verify/schemas/partman/procedures/run_maintenance_by_id.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT has_function_privilege('partman.run_maintenance_by_id(uuid, boolean)', 'execute'); +SELECT assert_function('partman.run_maintenance_by_id(uuid, bool)'::regprocedure); ROLLBACK; diff --git a/packages/partman/verify/schemas/partman/procedures/verify_parent_by_id.sql b/packages/partman/verify/schemas/partman/procedures/verify_parent_by_id.sql index 9704d0c98..cd70beb9a 100644 --- a/packages/partman/verify/schemas/partman/procedures/verify_parent_by_id.sql +++ b/packages/partman/verify/schemas/partman/procedures/verify_parent_by_id.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT has_function_privilege('partman.verify_parent_by_id(uuid)', 'execute'); +SELECT assert_function('partman.verify_parent_by_id(uuid)'::regprocedure); ROLLBACK; diff --git a/packages/partman/verify/schemas/partman/triggers/after_delete_partition.sql b/packages/partman/verify/schemas/partman/triggers/after_delete_partition.sql new file mode 100644 index 000000000..812c18645 --- /dev/null +++ b/packages/partman/verify/schemas/partman/triggers/after_delete_partition.sql @@ -0,0 +1,10 @@ +-- Verify schemas/partman/triggers/after_delete_partition on pg + +BEGIN; + +SELECT 1/count(*) + FROM pg_trigger + WHERE tgname = 'partman_after_delete_partition' + AND tgrelid = 'metaschema_public.partition'::regclass; + +ROLLBACK; diff --git a/packages/stamps/Makefile b/packages/stamps/Makefile index a85f4aae6..b6930d651 100644 --- a/packages/stamps/Makefile +++ b/packages/stamps/Makefile @@ -1,5 +1,5 @@ EXTENSION = pgpm-stamps -DATA = sql/pgpm-stamps--0.36.0.sql +DATA = sql/pgpm-stamps--0.39.0.sql PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) diff --git a/packages/stamps/package.json b/packages/stamps/package.json index 5f38f908b..c566e2cd3 100644 --- a/packages/stamps/package.json +++ b/packages/stamps/package.json @@ -1,6 +1,6 @@ { "name": "@pgpm/stamps", - "version": "0.36.0", + "version": "0.39.0", "description": "Timestamp utilities and audit trail functions for PostgreSQL", "author": "Dan Lynch ", "contributors": [ diff --git a/packages/stamps/pgpm-stamps.control b/packages/stamps/pgpm-stamps.control index 135d3de3d..8c9abeaaf 100644 --- a/packages/stamps/pgpm-stamps.control +++ b/packages/stamps/pgpm-stamps.control @@ -1,6 +1,6 @@ # pgpm-stamps extension comment = 'pgpm-stamps extension' -default_version = '0.36.0' +default_version = '0.39.0' module_pathname = '$libdir/pgpm-stamps' requires = 'plpgsql,pgpm-jwt-claims,pgpm-verify' relocatable = false diff --git a/packages/stamps/revert/schemas/stamps/procedures/utils.sql b/packages/stamps/revert/schemas/stamps/procedures/utils.sql index ddbf19cf1..3330605db 100644 --- a/packages/stamps/revert/schemas/stamps/procedures/utils.sql +++ b/packages/stamps/revert/schemas/stamps/procedures/utils.sql @@ -2,7 +2,7 @@ BEGIN; -DROP FUNCTION stamps.peoplestamps(); DROP FUNCTION stamps.timestamps(); +DROP FUNCTION stamps.peoplestamps(); COMMIT; diff --git a/packages/stamps/sql/pgpm-stamps--0.36.0.bundle.tar.gz b/packages/stamps/sql/pgpm-stamps--0.36.0.bundle.tar.gz deleted file mode 100644 index f73442c69..000000000 Binary files a/packages/stamps/sql/pgpm-stamps--0.36.0.bundle.tar.gz and /dev/null differ diff --git a/packages/stamps/sql/pgpm-stamps--0.39.0.bundle.tar.gz b/packages/stamps/sql/pgpm-stamps--0.39.0.bundle.tar.gz new file mode 100644 index 000000000..2ec292737 Binary files /dev/null and b/packages/stamps/sql/pgpm-stamps--0.39.0.bundle.tar.gz differ diff --git a/packages/stamps/sql/pgpm-stamps--0.36.0.sql b/packages/stamps/sql/pgpm-stamps--0.39.0.sql similarity index 100% rename from packages/stamps/sql/pgpm-stamps--0.36.0.sql rename to packages/stamps/sql/pgpm-stamps--0.39.0.sql diff --git a/packages/stamps/verify/schemas/stamps/procedures/utils.sql b/packages/stamps/verify/schemas/stamps/procedures/utils.sql index c418090f8..db16a2aa2 100644 --- a/packages/stamps/verify/schemas/stamps/procedures/utils.sql +++ b/packages/stamps/verify/schemas/stamps/procedures/utils.sql @@ -2,7 +2,7 @@ BEGIN; -SELECT verify_function ('stamps.peoplestamps'); -SELECT verify_function ('stamps.timestamps'); +SELECT assert_function('stamps.peoplestamps()'::regprocedure); +SELECT assert_function('stamps.timestamps()'::regprocedure); ROLLBACK; diff --git a/packages/stamps/verify/schemas/stamps/schema.sql b/packages/stamps/verify/schemas/stamps/schema.sql index c5e61273b..2d16c7d87 100644 --- a/packages/stamps/verify/schemas/stamps/schema.sql +++ b/packages/stamps/verify/schemas/stamps/schema.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_schema ('stamps'); +SELECT assert_schema('stamps'::regnamespace); ROLLBACK; diff --git a/packages/totp/Makefile b/packages/totp/Makefile index 7e62d6f06..7eb858374 100644 --- a/packages/totp/Makefile +++ b/packages/totp/Makefile @@ -1,5 +1,5 @@ EXTENSION = pgpm-totp -DATA = sql/pgpm-totp--0.36.0.sql +DATA = sql/pgpm-totp--0.39.0.sql PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) diff --git a/packages/totp/__tests__/totp.test.ts b/packages/totp/__tests__/totp.test.ts index 460ce211c..bc1d8cd2f 100644 --- a/packages/totp/__tests__/totp.test.ts +++ b/packages/totp/__tests__/totp.test.ts @@ -1,4 +1,5 @@ import { getConnections, PgTestClient } from 'pgsql-test'; +import cases from 'jest-in-case'; let pg: PgTestClient; let teardown: () => Promise; @@ -24,3 +25,31 @@ it('totp.generate + totp.verify basic', async () => { expect(typeof generate).toBe('string'); expect(verify).toBe(true); }); + +it('totp.generate handles keys whose bytes contain a NUL', async () => { + // decodes to five 0x00 bytes; chr(0) would raise "null character not permitted" + const { generate } = await pg.one( + `SELECT totp.generate($1::text) AS generate`, + ['AAAAAAAA'] + ); + expect(generate).toMatch(/^\d{6}$/); +}); + +cases( + 'totp.base32_to_hex cases', + async (opts: { name: string; result: string }) => { + const { base32_to_hex } = await pg.one( + `SELECT totp.base32_to_hex($1::text) AS base32_to_hex`, + [opts.name] + ); + expect(base32_to_hex).toEqual(opts.result); + }, + [ + { result: '', name: '' }, + { result: '666f6f626172', name: 'MZXW6YTBOI======' }, + { result: '666f6f626172', name: 'mzxw6ytboi' }, + { result: '0000000000', name: 'AAAAAAAA' }, + { result: '00ff00ff00', name: 'AD7QB7YA' }, + { result: '48656c6c6f21deadbeef', name: 'JBSWY3DPEHPK3PXP' } + ] +); diff --git a/packages/totp/deploy/schemas/totp/procedures/generate_totp.sql b/packages/totp/deploy/schemas/totp/procedures/generate_totp.sql index 668f99841..948c362d8 100644 --- a/packages/totp/deploy/schemas/totp/procedures/generate_totp.sql +++ b/packages/totp/deploy/schemas/totp/procedures/generate_totp.sql @@ -33,26 +33,52 @@ END; $$ LANGUAGE 'plpgsql' IMMUTABLE; +-- Decode a base32 secret straight to its hex representation. We intentionally +-- do NOT route through base32.decode(), which materialises the decoded bytes as +-- text via chr(): a decoded 0x00 byte raises "null character not permitted", so +-- TOTP generation failed for any secret whose bytes contain a null (roughly one +-- in twenty random secrets). Emitting hex per byte is binary-safe and produces +-- exactly the bytes base32.decode intends, so existing codes are unchanged. CREATE FUNCTION totp.base32_to_hex ( input text ) returns text as $$ -DECLARE - output text[]; - decoded text = base32.decode(input); - len int = character_length(decoded); - hx text; +DECLARE + i int; + len int; + num int; + clean text; + value int = 0; + bits int = 0; + index int = 0; + byte int; + output text = ''; BEGIN + IF (character_length(input) = 0) THEN + RETURN ''; + END IF; + + IF (NOT base32.valid(input)) THEN + RAISE EXCEPTION 'INVALID_BASE32'; + END IF; + + clean = upper(replace(input, '=', '')); + len = character_length(clean); + num = len * 5 / 8; FOR i IN 1 .. len LOOP - hx = to_hex(ascii(substring(decoded from i for 1)))::text; - IF (character_length(hx) = 1) THEN - -- if it is odd number of digits, pad a 0 so it can later - hx = '0' || hx; + value = (value << 5) | base32.base32_alphabet_to_decimal_int(substring(clean from i for 1)); + bits = bits + 5; + IF (bits >= 8) THEN + IF (index < num) THEN + byte = base32.zero_fill(value, (bits - 8)) & 255; + output = output || lpad(to_hex(byte), 2, '0'); + index = index + 1; + END IF; + bits = bits - 8; END IF; - output = array_append(output, hx); END LOOP; - RETURN array_to_string(output, ''); + RETURN output; END; $$ LANGUAGE 'plpgsql' IMMUTABLE; diff --git a/packages/totp/package.json b/packages/totp/package.json index dd6fcdf8f..b8cd7a536 100644 --- a/packages/totp/package.json +++ b/packages/totp/package.json @@ -1,6 +1,6 @@ { "name": "@pgpm/totp", - "version": "0.36.0", + "version": "0.39.0", "description": "Time-based One-Time Password (TOTP) authentication", "author": "Dan Lynch ", "contributors": [ diff --git a/packages/totp/pgpm-totp.control b/packages/totp/pgpm-totp.control index 9a28313cb..26ea067cb 100644 --- a/packages/totp/pgpm-totp.control +++ b/packages/totp/pgpm-totp.control @@ -1,6 +1,6 @@ # pgpm-totp extension comment = 'pgpm-totp extension' -default_version = '0.36.0' +default_version = '0.39.0' module_pathname = '$libdir/pgpm-totp' requires = 'pgcrypto,plpgsql,pgpm-base32,pgpm-verify' relocatable = false diff --git a/packages/totp/revert/schemas/totp/procedures/generate_totp.sql b/packages/totp/revert/schemas/totp/procedures/generate_totp.sql index 10261e5ca..c87d5dcc4 100644 --- a/packages/totp/revert/schemas/totp/procedures/generate_totp.sql +++ b/packages/totp/revert/schemas/totp/procedures/generate_totp.sql @@ -2,13 +2,13 @@ BEGIN; -DROP FUNCTION totp.url; -DROP FUNCTION totp.verify; -DROP FUNCTION totp.timing_safe_equals(a text, b text); -DROP FUNCTION totp.timing_safe_equals(a bytea, b bytea); -DROP FUNCTION totp.generate; -DROP FUNCTION totp.hotp; -DROP FUNCTION totp.base32_to_hex; -DROP FUNCTION totp.pad_secret; +DROP FUNCTION totp.url(text, text, int4, text); +DROP FUNCTION totp.verify(text, text, int4, int4, timestamptz, text, text, int4); +DROP FUNCTION totp.timing_safe_equals(text, text); +DROP FUNCTION totp.timing_safe_equals(bytea, bytea); +DROP FUNCTION totp.generate(text, int4, int4, timestamptz, text, text, int4); +DROP FUNCTION totp.hotp(bytea, int4, int4, text); +DROP FUNCTION totp.base32_to_hex(text); +DROP FUNCTION totp.pad_secret(bytea, int4); COMMIT; diff --git a/packages/totp/revert/schemas/totp/procedures/random_base32.sql b/packages/totp/revert/schemas/totp/procedures/random_base32.sql index 22fcac9aa..e267dcf1a 100644 --- a/packages/totp/revert/schemas/totp/procedures/random_base32.sql +++ b/packages/totp/revert/schemas/totp/procedures/random_base32.sql @@ -2,7 +2,7 @@ BEGIN; -DROP FUNCTION totp.generate_secret; -DROP FUNCTION totp.random_base32; +DROP FUNCTION totp.generate_secret(text); +DROP FUNCTION totp.random_base32(int4); COMMIT; diff --git a/packages/totp/revert/schemas/totp/procedures/urlencode.sql b/packages/totp/revert/schemas/totp/procedures/urlencode.sql index 5fd2c85f2..4b7db22fd 100644 --- a/packages/totp/revert/schemas/totp/procedures/urlencode.sql +++ b/packages/totp/revert/schemas/totp/procedures/urlencode.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION totp.urlencode; +DROP FUNCTION totp.urlencode(text); COMMIT; diff --git a/packages/totp/sql/pgpm-totp--0.36.0.bundle.tar.gz b/packages/totp/sql/pgpm-totp--0.36.0.bundle.tar.gz deleted file mode 100644 index 16e39f640..000000000 Binary files a/packages/totp/sql/pgpm-totp--0.36.0.bundle.tar.gz and /dev/null differ diff --git a/packages/totp/sql/pgpm-totp--0.39.0.bundle.tar.gz b/packages/totp/sql/pgpm-totp--0.39.0.bundle.tar.gz new file mode 100644 index 000000000..00353b91a Binary files /dev/null and b/packages/totp/sql/pgpm-totp--0.39.0.bundle.tar.gz differ diff --git a/packages/totp/sql/pgpm-totp--0.36.0.sql b/packages/totp/sql/pgpm-totp--0.39.0.sql similarity index 88% rename from packages/totp/sql/pgpm-totp--0.36.0.sql rename to packages/totp/sql/pgpm-totp--0.39.0.sql index f377deaff..f1d1ad578 100644 --- a/packages/totp/sql/pgpm-totp--0.36.0.sql +++ b/packages/totp/sql/pgpm-totp--0.39.0.sql @@ -60,23 +60,43 @@ $EOFCODE$ LANGUAGE plpgsql IMMUTABLE; CREATE FUNCTION totp.base32_to_hex( input text ) RETURNS text AS $EOFCODE$ -DECLARE - output text[]; - decoded text = base32.decode(input); - len int = character_length(decoded); - hx text; +DECLARE + i int; + len int; + num int; + clean text; + value int = 0; + bits int = 0; + index int = 0; + byte int; + output text = ''; BEGIN + IF (character_length(input) = 0) THEN + RETURN ''; + END IF; + + IF (NOT base32.valid(input)) THEN + RAISE EXCEPTION 'INVALID_BASE32'; + END IF; + + clean = upper(replace(input, '=', '')); + len = character_length(clean); + num = len * 5 / 8; FOR i IN 1 .. len LOOP - hx = to_hex(ascii(substring(decoded from i for 1)))::text; - IF (character_length(hx) = 1) THEN - -- if it is odd number of digits, pad a 0 so it can later - hx = '0' || hx; + value = (value << 5) | base32.base32_alphabet_to_decimal_int(substring(clean from i for 1)); + bits = bits + 5; + IF (bits >= 8) THEN + IF (index < num) THEN + byte = base32.zero_fill(value, (bits - 8)) & 255; + output = output || lpad(to_hex(byte), 2, '0'); + index = index + 1; + END IF; + bits = bits - 8; END IF; - output = array_append(output, hx); END LOOP; - RETURN array_to_string(output, ''); + RETURN output; END; $EOFCODE$ LANGUAGE plpgsql IMMUTABLE; diff --git a/packages/totp/verify/schemas/totp/procedures/generate_totp.sql b/packages/totp/verify/schemas/totp/procedures/generate_totp.sql index 7038d4f6c..7de5a2d51 100644 --- a/packages/totp/verify/schemas/totp/procedures/generate_totp.sql +++ b/packages/totp/verify/schemas/totp/procedures/generate_totp.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('totp.generate'); +SELECT assert_function('totp.generate(text, int4, int4, timestamptz, text, text, int4)'::regprocedure); ROLLBACK; diff --git a/packages/totp/verify/schemas/totp/procedures/random_base32.sql b/packages/totp/verify/schemas/totp/procedures/random_base32.sql index e518b9ae8..6dd44c71d 100644 --- a/packages/totp/verify/schemas/totp/procedures/random_base32.sql +++ b/packages/totp/verify/schemas/totp/procedures/random_base32.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('totp.random_base32'); +SELECT assert_function('totp.random_base32(int4)'::regprocedure); ROLLBACK; diff --git a/packages/totp/verify/schemas/totp/procedures/urlencode.sql b/packages/totp/verify/schemas/totp/procedures/urlencode.sql index 78097d071..70863778d 100644 --- a/packages/totp/verify/schemas/totp/procedures/urlencode.sql +++ b/packages/totp/verify/schemas/totp/procedures/urlencode.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('totp.urlencode'); +SELECT assert_function('totp.urlencode(text)'::regprocedure); ROLLBACK; diff --git a/packages/totp/verify/schemas/totp/schema.sql b/packages/totp/verify/schemas/totp/schema.sql index a26e1990c..367791740 100644 --- a/packages/totp/verify/schemas/totp/schema.sql +++ b/packages/totp/verify/schemas/totp/schema.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_schema ('totp'); +SELECT assert_schema('totp'::regnamespace); ROLLBACK; diff --git a/packages/types/Makefile b/packages/types/Makefile index f825b9ac4..88ea29269 100644 --- a/packages/types/Makefile +++ b/packages/types/Makefile @@ -1,5 +1,5 @@ EXTENSION = pgpm-types -DATA = sql/pgpm-types--0.36.0.sql +DATA = sql/pgpm-types--0.39.0.sql PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) diff --git a/packages/types/package.json b/packages/types/package.json index 1e0b0d558..8bd79f69d 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@pgpm/types", - "version": "0.36.0", + "version": "0.39.0", "description": "Core PostgreSQL data types with deploy/verify/revert SQL scripts", "author": "Dan Lynch ", "contributors": [ diff --git a/packages/types/pgpm-types.control b/packages/types/pgpm-types.control index 51cb82c2a..c0eda6543 100644 --- a/packages/types/pgpm-types.control +++ b/packages/types/pgpm-types.control @@ -1,6 +1,6 @@ # pgpm-types extension comment = 'pgpm-types extension' -default_version = '0.36.0' +default_version = '0.39.0' module_pathname = '$libdir/pgpm-types' requires = 'plpgsql,citext,pgpm-verify' relocatable = false diff --git a/packages/types/sql/pgpm-types--0.36.0.bundle.tar.gz b/packages/types/sql/pgpm-types--0.36.0.bundle.tar.gz deleted file mode 100644 index ced2efb2f..000000000 Binary files a/packages/types/sql/pgpm-types--0.36.0.bundle.tar.gz and /dev/null differ diff --git a/packages/types/sql/pgpm-types--0.39.0.bundle.tar.gz b/packages/types/sql/pgpm-types--0.39.0.bundle.tar.gz new file mode 100644 index 000000000..aa5ddf2c6 Binary files /dev/null and b/packages/types/sql/pgpm-types--0.39.0.bundle.tar.gz differ diff --git a/packages/types/sql/pgpm-types--0.36.0.sql b/packages/types/sql/pgpm-types--0.39.0.sql similarity index 100% rename from packages/types/sql/pgpm-types--0.36.0.sql rename to packages/types/sql/pgpm-types--0.39.0.sql diff --git a/packages/types/verify/schemas/public/domains/attachment.sql b/packages/types/verify/schemas/public/domains/attachment.sql index 34c05d9f8..5495290a6 100644 --- a/packages/types/verify/schemas/public/domains/attachment.sql +++ b/packages/types/verify/schemas/public/domains/attachment.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_domain ('public.attachment'); +SELECT assert_domain('public.attachment'::regtype, 'text'::regtype, _constraints => 1); ROLLBACK; diff --git a/packages/types/verify/schemas/public/domains/email.sql b/packages/types/verify/schemas/public/domains/email.sql index 62d87b47a..de9f8063d 100644 --- a/packages/types/verify/schemas/public/domains/email.sql +++ b/packages/types/verify/schemas/public/domains/email.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_domain ('public.email'); +SELECT assert_domain('public.email'::regtype, 'citext'::regtype, _constraints => 1); ROLLBACK; diff --git a/packages/types/verify/schemas/public/domains/hostname.sql b/packages/types/verify/schemas/public/domains/hostname.sql index e6da0854e..ea19cf95f 100644 --- a/packages/types/verify/schemas/public/domains/hostname.sql +++ b/packages/types/verify/schemas/public/domains/hostname.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_domain ('public.hostname'); +SELECT assert_domain('public.hostname'::regtype, 'text'::regtype, _constraints => 1); ROLLBACK; diff --git a/packages/types/verify/schemas/public/domains/image.sql b/packages/types/verify/schemas/public/domains/image.sql index 85d7e8989..e917b1c47 100644 --- a/packages/types/verify/schemas/public/domains/image.sql +++ b/packages/types/verify/schemas/public/domains/image.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_domain ('public.image'); +SELECT assert_domain('public.image'::regtype, 'jsonb'::regtype, _constraints => 1); ROLLBACK; diff --git a/packages/types/verify/schemas/public/domains/origin.sql b/packages/types/verify/schemas/public/domains/origin.sql index 2d69930f8..3b061ca27 100644 --- a/packages/types/verify/schemas/public/domains/origin.sql +++ b/packages/types/verify/schemas/public/domains/origin.sql @@ -2,7 +2,6 @@ BEGIN; -SELECT verify_type ('public.origin'); +SELECT assert_domain('public.origin'::regtype, 'text'::regtype, _constraints => 1); ROLLBACK; - diff --git a/packages/types/verify/schemas/public/domains/upload.sql b/packages/types/verify/schemas/public/domains/upload.sql index 68f70d837..b3c42e669 100644 --- a/packages/types/verify/schemas/public/domains/upload.sql +++ b/packages/types/verify/schemas/public/domains/upload.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_domain ('public.upload'); +SELECT assert_domain('public.upload'::regtype, 'jsonb'::regtype, _constraints => 1); ROLLBACK; diff --git a/packages/types/verify/schemas/public/domains/url.sql b/packages/types/verify/schemas/public/domains/url.sql index a2251df00..422673d53 100644 --- a/packages/types/verify/schemas/public/domains/url.sql +++ b/packages/types/verify/schemas/public/domains/url.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_domain ('public.url'); +SELECT assert_domain('public.url'::regtype, 'text'::regtype, _constraints => 1); ROLLBACK; diff --git a/packages/utils/Makefile b/packages/utils/Makefile index 73d404020..c810dc7d6 100644 --- a/packages/utils/Makefile +++ b/packages/utils/Makefile @@ -1,5 +1,5 @@ EXTENSION = pgpm-utils -DATA = sql/pgpm-utils--0.36.0.sql +DATA = sql/pgpm-utils--0.39.0.sql PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) diff --git a/packages/utils/package.json b/packages/utils/package.json index fc273f622..765dfcd7d 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "@pgpm/utils", - "version": "0.36.0", + "version": "0.39.0", "description": "General utility functions for PostgreSQL extensions", "author": "Dan Lynch ", "contributors": [ diff --git a/packages/utils/pgpm-utils.control b/packages/utils/pgpm-utils.control index 7ac6609a9..9924e466e 100644 --- a/packages/utils/pgpm-utils.control +++ b/packages/utils/pgpm-utils.control @@ -1,6 +1,6 @@ # pgpm-utils extension comment = 'pgpm-utils extension' -default_version = '0.36.0' +default_version = '0.39.0' module_pathname = '$libdir/pgpm-utils' requires = 'plpgsql,pgpm-verify' relocatable = false diff --git a/packages/utils/revert/schemas/utils/procedures/enforce_identity_providers_quota.sql b/packages/utils/revert/schemas/utils/procedures/enforce_identity_providers_quota.sql index 0dba88b12..aca7a0a4d 100644 --- a/packages/utils/revert/schemas/utils/procedures/enforce_identity_providers_quota.sql +++ b/packages/utils/revert/schemas/utils/procedures/enforce_identity_providers_quota.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION utils.enforce_identity_providers_quota; +DROP FUNCTION utils.enforce_identity_providers_quota(); COMMIT; diff --git a/packages/utils/revert/schemas/utils/procedures/ensure_singleton.sql b/packages/utils/revert/schemas/utils/procedures/ensure_singleton.sql index a5cf6c490..4c73a05a3 100644 --- a/packages/utils/revert/schemas/utils/procedures/ensure_singleton.sql +++ b/packages/utils/revert/schemas/utils/procedures/ensure_singleton.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION utils.ensure_singleton; +DROP FUNCTION utils.ensure_singleton(); COMMIT; diff --git a/packages/utils/revert/schemas/utils/procedures/mask_pad.sql b/packages/utils/revert/schemas/utils/procedures/mask_pad.sql index cecca273d..fbab5820c 100644 --- a/packages/utils/revert/schemas/utils/procedures/mask_pad.sql +++ b/packages/utils/revert/schemas/utils/procedures/mask_pad.sql @@ -2,6 +2,7 @@ BEGIN; -DROP FUNCTION utils.mask_pad; +DROP FUNCTION utils.bitmask_pad(varbit, int4, text); +DROP FUNCTION utils.mask_pad(text, int4, text); COMMIT; diff --git a/packages/utils/revert/schemas/utils/procedures/throw.sql b/packages/utils/revert/schemas/utils/procedures/throw.sql index fd7642a52..203513e64 100644 --- a/packages/utils/revert/schemas/utils/procedures/throw.sql +++ b/packages/utils/revert/schemas/utils/procedures/throw.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION utils.throw; +DROP FUNCTION utils.throw(); COMMIT; diff --git a/packages/utils/sql/pgpm-utils--0.36.0.bundle.tar.gz b/packages/utils/sql/pgpm-utils--0.36.0.bundle.tar.gz deleted file mode 100644 index af81178e3..000000000 Binary files a/packages/utils/sql/pgpm-utils--0.36.0.bundle.tar.gz and /dev/null differ diff --git a/packages/utils/sql/pgpm-utils--0.39.0.bundle.tar.gz b/packages/utils/sql/pgpm-utils--0.39.0.bundle.tar.gz new file mode 100644 index 000000000..c2cdefa13 Binary files /dev/null and b/packages/utils/sql/pgpm-utils--0.39.0.bundle.tar.gz differ diff --git a/packages/utils/sql/pgpm-utils--0.36.0.sql b/packages/utils/sql/pgpm-utils--0.39.0.sql similarity index 100% rename from packages/utils/sql/pgpm-utils--0.36.0.sql rename to packages/utils/sql/pgpm-utils--0.39.0.sql diff --git a/packages/utils/verify/schemas/utils/procedures/enforce_identity_providers_quota.sql b/packages/utils/verify/schemas/utils/procedures/enforce_identity_providers_quota.sql index 14e01be8b..81adf5cbb 100644 --- a/packages/utils/verify/schemas/utils/procedures/enforce_identity_providers_quota.sql +++ b/packages/utils/verify/schemas/utils/procedures/enforce_identity_providers_quota.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('utils.enforce_identity_providers_quota'); +SELECT assert_function('utils.enforce_identity_providers_quota()'::regprocedure); ROLLBACK; diff --git a/packages/utils/verify/schemas/utils/procedures/ensure_singleton.sql b/packages/utils/verify/schemas/utils/procedures/ensure_singleton.sql index d079b5670..99a1884e8 100644 --- a/packages/utils/verify/schemas/utils/procedures/ensure_singleton.sql +++ b/packages/utils/verify/schemas/utils/procedures/ensure_singleton.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('utils.ensure_singleton'); +SELECT assert_function('utils.ensure_singleton()'::regprocedure); ROLLBACK; diff --git a/packages/utils/verify/schemas/utils/procedures/mask_pad.sql b/packages/utils/verify/schemas/utils/procedures/mask_pad.sql index d77848ee6..408594419 100644 --- a/packages/utils/verify/schemas/utils/procedures/mask_pad.sql +++ b/packages/utils/verify/schemas/utils/procedures/mask_pad.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('utils.mask_pad'); +SELECT assert_function('utils.mask_pad(text, int4, text)'::regprocedure); ROLLBACK; diff --git a/packages/utils/verify/schemas/utils/procedures/throw.sql b/packages/utils/verify/schemas/utils/procedures/throw.sql index 2773912af..0e0407701 100644 --- a/packages/utils/verify/schemas/utils/procedures/throw.sql +++ b/packages/utils/verify/schemas/utils/procedures/throw.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('utils.throw'); +SELECT assert_function('utils.throw()'::regprocedure); ROLLBACK; diff --git a/packages/utils/verify/schemas/utils/schema.sql b/packages/utils/verify/schemas/utils/schema.sql index c475519ce..451cb92ac 100644 --- a/packages/utils/verify/schemas/utils/schema.sql +++ b/packages/utils/verify/schemas/utils/schema.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_schema ('utils'); +SELECT assert_schema('utils'::regnamespace); ROLLBACK; diff --git a/packages/uuid/Makefile b/packages/uuid/Makefile index e872fcc64..da84fc954 100644 --- a/packages/uuid/Makefile +++ b/packages/uuid/Makefile @@ -1,5 +1,5 @@ EXTENSION = pgpm-uuid -DATA = sql/pgpm-uuid--0.36.0.sql +DATA = sql/pgpm-uuid--0.39.0.sql PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) diff --git a/packages/uuid/package.json b/packages/uuid/package.json index 8124750c8..6621ceacf 100644 --- a/packages/uuid/package.json +++ b/packages/uuid/package.json @@ -1,6 +1,6 @@ { "name": "@pgpm/uuid", - "version": "0.36.0", + "version": "0.39.0", "description": "UUID utilities and extensions for PostgreSQL", "author": "Dan Lynch ", "contributors": [ diff --git a/packages/uuid/pgpm-uuid.control b/packages/uuid/pgpm-uuid.control index d61bbcd96..14f77b064 100644 --- a/packages/uuid/pgpm-uuid.control +++ b/packages/uuid/pgpm-uuid.control @@ -1,6 +1,6 @@ # pgpm-uuid extension comment = 'pgpm-uuid extension' -default_version = '0.36.0' +default_version = '0.39.0' module_pathname = '$libdir/pgpm-uuid' requires = 'pgcrypto,plpgsql,uuid-ossp,hstore,pgpm-verify' relocatable = false diff --git a/packages/uuid/revert/schemas/uuids/procedures/pseudo_order_seed_uuid.sql b/packages/uuid/revert/schemas/uuids/procedures/pseudo_order_seed_uuid.sql index dfb77b5c7..1c7b77443 100644 --- a/packages/uuid/revert/schemas/uuids/procedures/pseudo_order_seed_uuid.sql +++ b/packages/uuid/revert/schemas/uuids/procedures/pseudo_order_seed_uuid.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION uuids.pseudo_order_seed_uuid; +DROP FUNCTION uuids.pseudo_order_seed_uuid(text); COMMIT; diff --git a/packages/uuid/revert/schemas/uuids/procedures/pseudo_order_uuid.sql b/packages/uuid/revert/schemas/uuids/procedures/pseudo_order_uuid.sql index 79d1c887f..01c2cb1c5 100644 --- a/packages/uuid/revert/schemas/uuids/procedures/pseudo_order_uuid.sql +++ b/packages/uuid/revert/schemas/uuids/procedures/pseudo_order_uuid.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION uuids.pseudo_order_uuid; +DROP FUNCTION uuids.pseudo_order_uuid(); COMMIT; diff --git a/packages/uuid/revert/schemas/uuids/procedures/trigger_set_uuid_related_field.sql b/packages/uuid/revert/schemas/uuids/procedures/trigger_set_uuid_related_field.sql index 40b46093b..dbd87c893 100644 --- a/packages/uuid/revert/schemas/uuids/procedures/trigger_set_uuid_related_field.sql +++ b/packages/uuid/revert/schemas/uuids/procedures/trigger_set_uuid_related_field.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION uuids.trigger_set_uuid_related_field; +DROP FUNCTION uuids.trigger_set_uuid_related_field(); COMMIT; diff --git a/packages/uuid/revert/schemas/uuids/procedures/trigger_set_uuid_seed.sql b/packages/uuid/revert/schemas/uuids/procedures/trigger_set_uuid_seed.sql index 4d3b956d9..a6511b6b2 100644 --- a/packages/uuid/revert/schemas/uuids/procedures/trigger_set_uuid_seed.sql +++ b/packages/uuid/revert/schemas/uuids/procedures/trigger_set_uuid_seed.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION uuids.trigger_set_uuid_seed; +DROP FUNCTION uuids.trigger_set_uuid_seed(); COMMIT; diff --git a/packages/uuid/sql/pgpm-uuid--0.36.0.bundle.tar.gz b/packages/uuid/sql/pgpm-uuid--0.36.0.bundle.tar.gz deleted file mode 100644 index ab53d2272..000000000 Binary files a/packages/uuid/sql/pgpm-uuid--0.36.0.bundle.tar.gz and /dev/null differ diff --git a/packages/uuid/sql/pgpm-uuid--0.39.0.bundle.tar.gz b/packages/uuid/sql/pgpm-uuid--0.39.0.bundle.tar.gz new file mode 100644 index 000000000..57dbcffb4 Binary files /dev/null and b/packages/uuid/sql/pgpm-uuid--0.39.0.bundle.tar.gz differ diff --git a/packages/uuid/sql/pgpm-uuid--0.36.0.sql b/packages/uuid/sql/pgpm-uuid--0.39.0.sql similarity index 100% rename from packages/uuid/sql/pgpm-uuid--0.36.0.sql rename to packages/uuid/sql/pgpm-uuid--0.39.0.sql diff --git a/packages/uuid/verify/schemas/uuids/procedures/pseudo_order_seed_uuid.sql b/packages/uuid/verify/schemas/uuids/procedures/pseudo_order_seed_uuid.sql index 10d5635fe..11836b09d 100644 --- a/packages/uuid/verify/schemas/uuids/procedures/pseudo_order_seed_uuid.sql +++ b/packages/uuid/verify/schemas/uuids/procedures/pseudo_order_seed_uuid.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('uuids.pseudo_order_seed_uuid'); +SELECT assert_function('uuids.pseudo_order_seed_uuid(text)'::regprocedure); ROLLBACK; diff --git a/packages/uuid/verify/schemas/uuids/procedures/pseudo_order_uuid.sql b/packages/uuid/verify/schemas/uuids/procedures/pseudo_order_uuid.sql index 4bf9e5512..03ef8154e 100644 --- a/packages/uuid/verify/schemas/uuids/procedures/pseudo_order_uuid.sql +++ b/packages/uuid/verify/schemas/uuids/procedures/pseudo_order_uuid.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('uuids.pseudo_order_uuid'); +SELECT assert_function('uuids.pseudo_order_uuid()'::regprocedure); ROLLBACK; diff --git a/packages/uuid/verify/schemas/uuids/procedures/trigger_set_uuid_related_field.sql b/packages/uuid/verify/schemas/uuids/procedures/trigger_set_uuid_related_field.sql index 0b4b02c42..85a48f296 100644 --- a/packages/uuid/verify/schemas/uuids/procedures/trigger_set_uuid_related_field.sql +++ b/packages/uuid/verify/schemas/uuids/procedures/trigger_set_uuid_related_field.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('uuids.trigger_set_uuid_related_field'); +SELECT assert_function('uuids.trigger_set_uuid_related_field()'::regprocedure); ROLLBACK; diff --git a/packages/uuid/verify/schemas/uuids/procedures/trigger_set_uuid_seed.sql b/packages/uuid/verify/schemas/uuids/procedures/trigger_set_uuid_seed.sql index 72342b832..cc967f03a 100644 --- a/packages/uuid/verify/schemas/uuids/procedures/trigger_set_uuid_seed.sql +++ b/packages/uuid/verify/schemas/uuids/procedures/trigger_set_uuid_seed.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_function ('uuids.trigger_set_uuid_seed'); +SELECT assert_function('uuids.trigger_set_uuid_seed()'::regprocedure); ROLLBACK; diff --git a/packages/uuid/verify/schemas/uuids/schema.sql b/packages/uuid/verify/schemas/uuids/schema.sql index 62f1e2f9f..1f056ef86 100644 --- a/packages/uuid/verify/schemas/uuids/schema.sql +++ b/packages/uuid/verify/schemas/uuids/schema.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT verify_schema ('uuids'); +SELECT assert_schema('uuids'::regnamespace); ROLLBACK; diff --git a/packages/verify/Makefile b/packages/verify/Makefile index 7d1bb3bf4..c6e69ec39 100644 --- a/packages/verify/Makefile +++ b/packages/verify/Makefile @@ -1,5 +1,5 @@ EXTENSION = pgpm-verify -DATA = sql/pgpm-verify--0.36.0.sql +DATA = sql/pgpm-verify--0.39.0.sql PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) diff --git a/packages/verify/__tests__/assertions.test.ts b/packages/verify/__tests__/assertions.test.ts new file mode 100644 index 000000000..a4cbaec0b --- /dev/null +++ b/packages/verify/__tests__/assertions.test.ts @@ -0,0 +1,462 @@ +import { getConnections, PgTestClient } from 'pgsql-test'; + +let pg: PgTestClient; +let teardown: () => Promise; + +// The point of these assertions is that they can FAIL. Every describe below +// pairs the passing case with the drift a name-only verify_* check let through. +describe('catalog assertions', () => { + beforeAll(async () => { + ({ pg, teardown } = await getConnections()); + + await pg.any(`CREATE SCHEMA assert_demo`); + await pg.any(` + CREATE FUNCTION assert_demo.greet (name text) + RETURNS text AS $$ SELECT 'hi ' || name $$ LANGUAGE sql IMMUTABLE + `); + // Same name, different signature: the overload a name-only check confuses. + await pg.any(` + CREATE FUNCTION assert_demo.greet (name text, punct text) + RETURNS text AS $$ SELECT 'hi ' || name || punct $$ LANGUAGE sql IMMUTABLE + `); + await pg.any(` + CREATE FUNCTION assert_demo.elevated () + RETURNS void AS $$ BEGIN END $$ LANGUAGE plpgsql SECURITY DEFINER + `); + + await pg.any(`CREATE TABLE assert_demo.users (id uuid PRIMARY KEY, email text)`); + await pg.any(`CREATE UNIQUE INDEX users_email_key ON assert_demo.users (email)`); + await pg.any(`CREATE INDEX users_email_idx ON assert_demo.users (email text_pattern_ops)`); + await pg.any(` + CREATE TABLE assert_demo.events (id uuid, at timestamptz) + PARTITION BY RANGE (at) + `); + + await pg.any(` + CREATE VIEW assert_demo.user_emails WITH (security_invoker = true) + AS SELECT email FROM assert_demo.users + `); + await pg.any(` + CREATE VIEW assert_demo.definer_emails AS SELECT email FROM assert_demo.users + `); + await pg.any(` + CREATE MATERIALIZED VIEW assert_demo.user_count + AS SELECT count(*) FROM assert_demo.users + `); + + await pg.any(` + CREATE FUNCTION assert_demo.touch () + RETURNS trigger AS $$ BEGIN RETURN NEW; END $$ LANGUAGE plpgsql + `); + await pg.any(` + CREATE FUNCTION assert_demo.other () + RETURNS trigger AS $$ BEGIN RETURN NEW; END $$ LANGUAGE plpgsql + `); + await pg.any(` + CREATE TRIGGER stamps BEFORE INSERT OR UPDATE ON assert_demo.users + FOR EACH ROW EXECUTE PROCEDURE assert_demo.touch() + `); + + // A table-wide SELECT next to a column-scoped INSERT: the second is the + // shape information_schema.role_table_grants cannot report. + await pg.any(`CREATE ROLE grantee_demo`); + await pg.any(`GRANT SELECT ON assert_demo.users TO grantee_demo`); + await pg.any(`GRANT INSERT (email) ON assert_demo.users TO grantee_demo`); + + await pg.any(`CREATE DOMAIN assert_demo.email AS text CHECK (value ~ '@')`); + await pg.any(`CREATE DOMAIN assert_demo.loose AS text`); + await pg.any(`CREATE TYPE assert_demo.rgb AS ENUM ('r', 'g', 'b')`); + + await pg.any(`ALTER TABLE assert_demo.users ENABLE ROW LEVEL SECURITY`); + await pg.any(` + CREATE POLICY can_select ON assert_demo.users + FOR SELECT USING (email IS NOT NULL) + `); + await pg.any(` + CREATE POLICY block_all ON assert_demo.users + AS RESTRICTIVE FOR ALL USING (email IS NOT NULL) + `); + }); + + afterAll(async () => { + await pg.any(`DROP SCHEMA IF EXISTS assert_demo CASCADE`); + await pg.any(`DROP ROLE IF EXISTS grantee_demo`); + await teardown(); + }); + + const assertion = (sql: string) => pg.any(`SELECT ${sql}`); + + describe('assert_function', () => { + it('passes for the exact signature', async () => { + await expect( + assertion(`assert_function('assert_demo.greet(text)'::regprocedure, + 'text'::regtype, _returns_set => false, _security_definer => false, + _volatility => 'immutable')`) + ).resolves.toBeDefined(); + }); + + it('fails when the overload does not exist', async () => { + await expect( + assertion(`assert_function('assert_demo.greet(uuid)'::regprocedure)`) + ).rejects.toThrow(/does not exist/); + }); + + it('fails on the wrong return type', async () => { + await expect( + assertion( + `assert_function('assert_demo.greet(text)'::regprocedure, 'uuid'::regtype)` + ) + ).rejects.toThrow('must return uuid, found text'); + }); + + it('fails on an unintended SECURITY DEFINER', async () => { + await expect( + assertion( + `assert_function('assert_demo.elevated()'::regprocedure, _security_definer => false)` + ) + ).rejects.toThrow('must be SECURITY INVOKER'); + }); + + it('fails on drifted volatility', async () => { + await expect( + assertion( + `assert_function('assert_demo.greet(text)'::regprocedure, _volatility => 'stable')` + ) + ).rejects.toThrow('must be STABLE, found IMMUTABLE'); + }); + }); + + describe('assert_table', () => { + it('passes for an ordinary table', async () => { + await expect( + assertion(`assert_table('assert_demo.users'::regclass)`) + ).resolves.toBeDefined(); + }); + + it('passes for a partitioned table', async () => { + await expect( + assertion(`assert_table('assert_demo.events'::regclass, _partitioned => true)`) + ).resolves.toBeDefined(); + }); + + it('fails when the table is really a view', async () => { + await expect( + assertion(`assert_table('assert_demo.user_emails'::regclass)`) + ).rejects.toThrow('must be an ordinary table, found a view'); + }); + + it('fails when a table stopped being partitioned', async () => { + await expect( + assertion(`assert_table('assert_demo.users'::regclass, _partitioned => true)`) + ).rejects.toThrow('must be a partitioned table, found an ordinary table'); + }); + + it('fails when the relation does not exist', async () => { + await expect( + assertion(`assert_table('assert_demo.nope'::regclass)`) + ).rejects.toThrow(/does not exist/); + }); + }); + + describe('assert_view', () => { + it('passes for a security_invoker view', async () => { + await expect( + assertion( + `assert_view('assert_demo.user_emails'::regclass, _security_invoker => true)` + ) + ).resolves.toBeDefined(); + }); + + it('passes for a materialized view', async () => { + await expect( + assertion(`assert_view('assert_demo.user_count'::regclass, _materialized => true)`) + ).resolves.toBeDefined(); + }); + + it('fails when a view lost security_invoker', async () => { + await expect( + assertion( + `assert_view('assert_demo.definer_emails'::regclass, _security_invoker => true)` + ) + ).rejects.toThrow('must be a security_invoker view'); + }); + + it('fails when an ordinary view is expected to be materialized', async () => { + await expect( + assertion(`assert_view('assert_demo.user_emails'::regclass, _materialized => true)`) + ).rejects.toThrow('must be a materialized view, found a view'); + }); + }); + + describe('assert_index', () => { + it('passes for a unique index on the expected table', async () => { + await expect( + assertion(`assert_index('assert_demo.users_email_key'::regclass, + 'assert_demo.users'::regclass, _unique => true)`) + ).resolves.toBeDefined(); + }); + + it('fails when the index is not unique', async () => { + await expect( + assertion(`assert_index('assert_demo.users_email_idx'::regclass, + 'assert_demo.users'::regclass, _unique => true)`) + ).rejects.toThrow('must be UNIQUE'); + }); + + it('fails when the index covers another table', async () => { + await expect( + assertion(`assert_index('assert_demo.users_email_key'::regclass, + 'assert_demo.events'::regclass)`) + ).rejects.toThrow('must index assert_demo.events'); + }); + }); + + describe('assert_trigger', () => { + // BEFORE (2) | INSERT (4) | UPDATE (16) | ROW (1) + const tgtype = 2 | 4 | 16 | 1; + + it('passes for the deployed trigger', async () => { + await expect( + assertion(`assert_trigger('assert_demo.users'::regclass, 'stamps', + 'assert_demo.touch'::regproc, ${tgtype})`) + ).resolves.toBeDefined(); + }); + + it('fails when the trigger calls another function', async () => { + await expect( + assertion(`assert_trigger('assert_demo.users'::regclass, 'stamps', + 'assert_demo.other'::regproc)`) + ).rejects.toThrow('must call assert_demo.other, found assert_demo.touch'); + }); + + it('fails when the timing or events drifted', async () => { + // AFTER (0) | DELETE (8) | ROW (1) + await expect( + assertion( + `assert_trigger('assert_demo.users'::regclass, 'stamps', NULL, ${8 | 1})` + ) + ).rejects.toThrow( + 'must be AFTER DELETE FOR EACH ROW, found BEFORE INSERT OR UPDATE FOR EACH ROW' + ); + }); + + it('fails when the trigger is attached to another table', async () => { + await expect( + assertion(`assert_trigger('assert_demo.events'::regclass, 'stamps')`) + ).rejects.toThrow(/no data found|query returned no rows/i); + }); + + it('fails when the trigger is disabled', async () => { + await pg.any(`ALTER TABLE assert_demo.users DISABLE TRIGGER stamps`); + + try { + await expect( + assertion(`assert_trigger('assert_demo.users'::regclass, 'stamps')`) + ).rejects.toThrow('must be enabled'); + } finally { + await pg.any(`ALTER TABLE assert_demo.users ENABLE TRIGGER stamps`); + } + }); + }); + + describe('assert_policy', () => { + it('passes for the deployed policy', async () => { + await expect( + assertion(`assert_policy('assert_demo.users'::regclass, 'can_select', 'SELECT', + _permissive => true, _has_qual => true, _has_with_check => false)`) + ).resolves.toBeDefined(); + }); + + it('passes for a restrictive policy', async () => { + await expect( + assertion(`assert_policy('assert_demo.users'::regclass, 'block_all', 'ALL', + _permissive => false)`) + ).resolves.toBeDefined(); + }); + + it('fails when the policy widened to another command', async () => { + await expect( + assertion(`assert_policy('assert_demo.users'::regclass, 'can_select', 'ALL')`) + ).rejects.toThrow('must apply to ALL, found SELECT'); + }); + + it('fails when a restrictive policy turned permissive', async () => { + await expect( + assertion( + `assert_policy('assert_demo.users'::regclass, 'can_select', _permissive => false)` + ) + ).rejects.toThrow('must be RESTRICTIVE'); + }); + + it('fails when the USING clause was dropped', async () => { + await expect( + assertion( + `assert_policy('assert_demo.users'::regclass, 'can_select', _has_with_check => true)` + ) + ).rejects.toThrow('must carry a WITH CHECK clause'); + }); + }); + + describe('assert_schema', () => { + it('passes for a deployed schema', async () => { + await expect( + assertion(`assert_schema('assert_demo'::regnamespace)`) + ).resolves.toBeDefined(); + }); + + // The cast resolves the name, so the failure happens before the body runs — + // which is the point: the schema is a reference, not a compared string. + it('fails when the schema does not exist', async () => { + await expect( + assertion(`assert_schema('assert_nope'::regnamespace)`) + ).rejects.toThrow(/does not exist/); + }); + }); + + describe('assert_table_grant', () => { + it('passes for a table-wide grant', async () => { + await expect( + assertion(`assert_table_grant('assert_demo.users'::regclass, 'grantee_demo', 'SELECT')`) + ).resolves.toBeDefined(); + }); + + it('fails when the privilege was never granted', async () => { + await expect( + assertion(`assert_table_grant('assert_demo.users'::regclass, 'grantee_demo', 'DELETE')`) + ).rejects.toThrow(/must hold DELETE/); + }); + + // The drift the old check could not see: GRANT INSERT (email) is invisible + // to information_schema.role_table_grants, so verify_table_grant reported + // every column-scoped grant as missing. + it('passes for a column-scoped grant', async () => { + await expect( + assertion( + `assert_table_grant('assert_demo.users'::regclass, 'grantee_demo', 'INSERT', ARRAY['email'])` + ) + ).resolves.toBeDefined(); + }); + + it('fails when only some of the columns are granted', async () => { + await expect( + assertion( + `assert_table_grant('assert_demo.users'::regclass, 'grantee_demo', 'INSERT', ARRAY['email', 'id'])` + ) + ).rejects.toThrow(/must hold INSERT .*\(id\)/); + }); + + // A revoke asserts absence, so a grant that comes back is caught too. + it('asserts a privilege is absent', async () => { + await expect( + assertion( + `assert_table_grant('assert_demo.users'::regclass, 'grantee_demo', 'DELETE', NULL, false)` + ) + ).resolves.toBeDefined(); + + await expect( + assertion( + `assert_table_grant('assert_demo.users'::regclass, 'grantee_demo', 'SELECT', NULL, false)` + ) + ).rejects.toThrow(/must not hold SELECT/); + }); + }); + + describe('assert_table_security', () => { + it('passes when row level security is enabled', async () => { + await expect( + assertion(`assert_table_security('assert_demo.users'::regclass)`) + ).resolves.toBeDefined(); + }); + + // A table that quietly loses RLS still exists, so the name-only check passed. + it('fails when row level security is off', async () => { + await expect( + assertion(`assert_table_security('assert_demo.events'::regclass)`) + ).rejects.toThrow(/must be enabled/); + }); + + it('fails when the owner is not forced through the policies', async () => { + await expect( + assertion(`assert_table_security('assert_demo.users'::regclass, true, true)`) + ).rejects.toThrow(/must be forced/); + }); + }); + + describe('assert_function_grant', () => { + it('passes for a granted function', async () => { + await expect( + assertion( + `assert_function_grant('assert_demo.greet(text)'::regprocedure, 'grantee_demo')` + ) + ).resolves.toBeDefined(); + }); + + // The signature is resolved, so a grant checked against an overload that no + // longer exists raises instead of reporting a missing privilege. + it('fails when the signature does not exist', async () => { + await expect( + assertion( + `assert_function_grant('assert_demo.greet(uuid)'::regprocedure, 'grantee_demo')` + ) + ).rejects.toThrow(/does not exist/); + }); + + it('asserts a privilege is absent', async () => { + await expect( + assertion( + `assert_function_grant('assert_demo.greet(text)'::regprocedure, 'grantee_demo', 'EXECUTE', false)` + ) + ).rejects.toThrow(/must not hold EXECUTE/); + }); + }); + + describe('assert_domain', () => { + it('passes for the base type and constraint count', async () => { + await expect( + assertion( + `assert_domain('assert_demo.email'::regtype, 'text'::regtype, _constraints => 1)` + ) + ).resolves.toBeDefined(); + }); + + it('fails on the wrong base type', async () => { + await expect( + assertion(`assert_domain('assert_demo.email'::regtype, 'varchar'::regtype)`) + ).rejects.toThrow(/must be built on character varying/); + }); + + // A domain whose CHECK was dropped still exists and still has its base type, + // so it validates nothing while the name-only check keeps passing. + it('fails when the domain carries no constraint', async () => { + await expect( + assertion(`assert_domain('assert_demo.loose'::regtype, _constraints => 1)`) + ).rejects.toThrow(/must carry 1 constraint/); + }); + + it('fails when the name is a type rather than a domain', async () => { + await expect( + assertion(`assert_domain('assert_demo.rgb'::regtype)`) + ).rejects.toThrow(/must be a domain/); + }); + }); + + describe('assert_type', () => { + it('passes for a type of the expected kind', async () => { + await expect( + assertion(`assert_type('assert_demo.rgb'::regtype, 'e')`) + ).resolves.toBeDefined(); + }); + + it('fails on the wrong kind', async () => { + await expect( + assertion(`assert_type('assert_demo.rgb'::regtype, 'c')`) + ).rejects.toThrow(/must be typtype c/); + }); + + it('fails when the name is a domain', async () => { + await expect( + assertion(`assert_type('assert_demo.email'::regtype)`) + ).rejects.toThrow(/is a domain/); + }); + }); +}); diff --git a/packages/verify/__tests__/ext-verify.test.ts b/packages/verify/__tests__/ext-verify.test.ts deleted file mode 100644 index a07187470..000000000 --- a/packages/verify/__tests__/ext-verify.test.ts +++ /dev/null @@ -1,489 +0,0 @@ -import { getConnections, PgTestClient } from 'pgsql-test'; - -let pg: PgTestClient; -let teardown: () => Promise; - -describe('ext-verify utilities', () => { - beforeAll(async () => { - ({ pg, teardown } = await getConnections()); - }); - - afterAll(async () => { - await teardown(); - }); - - beforeEach(async () => { - await pg.beforeEach(); - }); - - afterEach(async () => { - await pg.afterEach(); - }); - - describe('helper functions', () => { - it('get_entity_from_str should extract entity name', async () => { - const tests = [ - ['public.users', 'users'], - ['users', 'users'], - ['schema.table_name', 'table_name'], - ['my_function', 'my_function'] - ]; - - for (const [input, expected] of tests) { - const [result] = await pg.any( - `SELECT get_entity_from_str($1) as entity`, - [input] - ); - expect(result.entity).toBe(expected); - } - }); - - it('get_schema_from_str should extract schema name', async () => { - const tests = [ - ['public.users', 'public'], - ['users', 'public'], - ['my_schema.table_name', 'my_schema'], - ['function_name', 'public'] - ]; - - for (const [input, expected] of tests) { - const [result] = await pg.any( - `SELECT get_schema_from_str($1) as schema_name`, - [input] - ); - expect(result.schema_name).toBe(expected); - } - }); - }); - - describe('schema verification', () => { - it('verify_schema should return true for existing schema', async () => { - const [result] = await pg.any( - `SELECT verify_schema('public') as verified` - ); - expect(result.verified).toBe(true); - }); - - it('verify_schema should throw for non-existent schema', async () => { - await expect( - pg.any(`SELECT verify_schema('nonexistent_schema')`) - ).rejects.toThrow('Nonexistent schema'); - }); - }); - - describe('table verification', () => { - beforeEach(async () => { - // Create a test table - await pg.any(` - CREATE TABLE test_table ( - id serial PRIMARY KEY, - name text NOT NULL, - email text UNIQUE - ) - `); - }); - - it('verify_table should return true for existing table', async () => { - const [result] = await pg.any( - `SELECT verify_table('test_table') as verified` - ); - expect(result.verified).toBe(true); - }); - - it('verify_table should work with schema-qualified names', async () => { - const [result] = await pg.any( - `SELECT verify_table('public.test_table') as verified` - ); - expect(result.verified).toBe(true); - }); - - it('verify_table should throw for non-existent table', async () => { - await expect( - pg.any(`SELECT verify_table('nonexistent_table')`) - ).rejects.toThrow('Nonexistent table'); - }); - }); - - describe('constraint verification', () => { - beforeEach(async () => { - await pg.any(` - CREATE TABLE test_constraints ( - id serial PRIMARY KEY, - name text NOT NULL, - email text UNIQUE, - age integer CHECK (age > 0) - ) - `); - }); - - it('verify_constraint should return true for existing constraint', async () => { - // Get the actual constraint name (PostgreSQL generates names) - const [constraint] = await pg.any(` - SELECT conname - FROM pg_constraint c - JOIN pg_class t ON c.conrelid = t.oid - WHERE t.relname = 'test_constraints' AND contype = 'u' - LIMIT 1 - `); - - if (constraint) { - const [result] = await pg.any( - `SELECT verify_constraint('test_constraints', $1) as verified`, - [constraint.conname] - ); - expect(result.verified).toBe(true); - } - }); - - it('verify_constraint should throw for non-existent constraint', async () => { - await expect( - pg.any(`SELECT verify_constraint('test_constraints', 'nonexistent_constraint')`) - ).rejects.toThrow('Nonexistent constraint'); - }); - }); - - describe('function verification', () => { - beforeEach(async () => { - await pg.any(` - CREATE OR REPLACE FUNCTION test_function(x integer) - RETURNS integer AS $$ - BEGIN - RETURN x * 2; - END; - $$ LANGUAGE plpgsql; - `); - }); - - it('verify_function should return true for existing function', async () => { - const [result] = await pg.any( - `SELECT verify_function('test_function') as verified` - ); - expect(result.verified).toBe(true); - }); - - it('verify_function should work with schema-qualified names', async () => { - const [result] = await pg.any( - `SELECT verify_function('public.test_function') as verified` - ); - expect(result.verified).toBe(true); - }); - - it('verify_function should throw for non-existent function', async () => { - await expect( - pg.any(`SELECT verify_function('nonexistent_function')`) - ).rejects.toThrow('Nonexistent function'); - }); - }); - - describe('view verification', () => { - beforeEach(async () => { - await pg.any(` - CREATE TABLE test_view_base ( - id serial PRIMARY KEY, - name text - ) - `); - - await pg.any(` - CREATE VIEW test_view AS - SELECT id, name FROM test_view_base WHERE name IS NOT NULL - `); - }); - - it('verify_view should return true for existing view', async () => { - const [result] = await pg.any( - `SELECT verify_view('test_view') as verified` - ); - expect(result.verified).toBe(true); - }); - - it('verify_view should work with schema-qualified names', async () => { - const [result] = await pg.any( - `SELECT verify_view('public.test_view') as verified` - ); - expect(result.verified).toBe(true); - }); - - it('verify_view should throw for non-existent view', async () => { - await expect( - pg.any(`SELECT verify_view('nonexistent_view')`) - ).rejects.toThrow('Nonexistent view'); - }); - }); - - describe('index verification', () => { - beforeEach(async () => { - await pg.any(` - CREATE TABLE test_index_table ( - id serial PRIMARY KEY, - name text, - email text - ) - `); - - await pg.any(` - CREATE INDEX test_custom_index ON test_index_table (name) - `); - }); - - it('verify_index should return true for existing index', async () => { - const [result] = await pg.any( - `SELECT verify_index('test_index_table', 'test_custom_index') as verified` - ); - expect(result.verified).toBe(true); - }); - - it('verify_index should throw for non-existent index', async () => { - await expect( - pg.any(`SELECT verify_index('test_index_table', 'nonexistent_index')`) - ).rejects.toThrow('Nonexistent index'); - }); - - it('list_indexes should return index information', async () => { - const results = await pg.any( - `SELECT * FROM list_indexes('test_index_table', 'test_custom_index')` - ); - - expect(results).toHaveLength(1); - expect(results[0].schema_name).toBe('public'); - expect(results[0].table_name).toBe('test_index_table'); - expect(results[0].index_name).toBe('test_custom_index'); - }); - }); - - describe('trigger verification', () => { - beforeEach(async () => { - await pg.any(` - CREATE TABLE test_trigger_table ( - id serial PRIMARY KEY, - name text, - updated_at timestamp DEFAULT now() - ) - `); - - await pg.any(` - CREATE OR REPLACE FUNCTION update_timestamp() - RETURNS trigger AS $$ - BEGIN - NEW.updated_at = now(); - RETURN NEW; - END; - $$ LANGUAGE plpgsql; - `); - - await pg.any(` - CREATE TRIGGER test_update_trigger - BEFORE UPDATE ON test_trigger_table - FOR EACH ROW EXECUTE FUNCTION update_timestamp() - `); - }); - - it('verify_trigger should return true for existing trigger', async () => { - const [result] = await pg.any( - `SELECT verify_trigger('test_update_trigger') as verified` - ); - expect(result.verified).toBe(true); - }); - - it('verify_trigger should throw for non-existent trigger', async () => { - await expect( - pg.any(`SELECT verify_trigger('nonexistent_trigger')`) - ).rejects.toThrow('Nonexistent trigger'); - }); - }); - - describe('type verification', () => { - beforeEach(async () => { - await pg.any(` - CREATE TYPE test_enum AS ENUM ('active', 'inactive', 'pending') - `); - - await pg.any(` - CREATE TYPE test_composite AS ( - name text, - value integer - ) - `); - }); - - it('verify_type should return true for existing enum type', async () => { - const [result] = await pg.any( - `SELECT verify_type('test_enum') as verified` - ); - expect(result.verified).toBe(true); - }); - - it('verify_type should return true for existing composite type', async () => { - const [result] = await pg.any( - `SELECT verify_type('test_composite') as verified` - ); - expect(result.verified).toBe(true); - }); - - it('verify_type should throw for non-existent type', async () => { - await expect( - pg.any(`SELECT verify_type('nonexistent_type')`) - ).rejects.toThrow('Nonexistent type'); - }); - }); - - describe('domain verification', () => { - beforeEach(async () => { - await pg.any(` - CREATE DOMAIN test_domain AS text CHECK (length(value) > 0) - `); - }); - - it('verify_domain should return true for existing domain', async () => { - const [result] = await pg.any( - `SELECT verify_domain('test_domain') as verified` - ); - expect(result.verified).toBe(true); - }); - - it('verify_domain should throw for non-existent domain', async () => { - await expect( - pg.any(`SELECT verify_domain('nonexistent_domain')`) - ).rejects.toThrow('Nonexistent type'); - }); - }); - - describe('role and membership verification', () => { - it('verify_role should return true for current user', async () => { - // Get current user - const [currentUser] = await pg.any(`SELECT current_user as username`); - - const [result] = await pg.any( - `SELECT verify_role($1) as verified`, - [currentUser.username] - ); - expect(result.verified).toBe(true); - }); - - it('verify_role should throw for non-existent role', async () => { - await expect( - pg.any(`SELECT verify_role('nonexistent_user_12345')`) - ).rejects.toThrow('Nonexistent user'); - }); - - it('list_memberships should return role memberships', async () => { - const [currentUser] = await pg.any(`SELECT current_user as username`); - - const results = await pg.any( - `SELECT * FROM list_memberships($1)`, - [currentUser.username] - ); - - // Should at least include the user themselves - expect(results.length).toBeGreaterThan(0); - const usernames = results.map(r => r.rolname); - expect(usernames).toContain(currentUser.username); - }); - }); - - describe('grant verification', () => { - beforeEach(async () => { - await pg.any(` - CREATE TABLE test_grants_table ( - id serial PRIMARY KEY, - data text - ) - `); - }); - - it('verify_table_grant should work for existing grants', async () => { - // Grant SELECT to current user (if not already granted) - const [currentUser] = await pg.any(`SELECT current_user as username`); - - // The table owner should have all privileges - const [result] = await pg.any( - `SELECT verify_table_grant('test_grants_table', 'INSERT', $1) as verified`, - [currentUser.username] - ); - expect(result.verified).toBe(true); - }); - }); - - describe('extension verification', () => { - it('verify_extension should return true for available extensions', async () => { - // Most PostgreSQL installations have the plpgsql extension available - const availableExtensions = await pg.any(` - SELECT name FROM pg_available_extensions - WHERE name IN ('plpgsql', 'uuid-ossp', 'pgcrypto') - LIMIT 1 - `); - - if (availableExtensions.length > 0) { - const [result] = await pg.any( - `SELECT verify_extension($1) as verified`, - [availableExtensions[0].name] - ); - expect(result.verified).toBe(true); - } else { - // Skip this test if no common extensions are available - expect(true).toBe(true); - } - }); - - it('verify_extension should throw for non-existent extension', async () => { - await expect( - pg.any(`SELECT verify_extension('definitely_nonexistent_extension_12345')`) - ).rejects.toThrow('Nonexistent extension'); - }); - }); - - describe('security verification', () => { - beforeEach(async () => { - await pg.any(` - CREATE TABLE test_security_table ( - id serial PRIMARY KEY, - user_id text, - data text - ) - `); - - // Enable row level security - await pg.any(`ALTER TABLE test_security_table ENABLE ROW LEVEL SECURITY`); - }); - - it('verify_security should return true for tables with RLS enabled', async () => { - const [result] = await pg.any( - `SELECT verify_security('test_security_table') as verified` - ); - expect(result.verified).toBe(true); - }); - }); - - describe('policy verification', () => { - beforeEach(async () => { - await pg.any(` - CREATE TABLE test_policy_table ( - id serial PRIMARY KEY, - user_id text, - data text - ) - `); - - await pg.any(`ALTER TABLE test_policy_table ENABLE ROW LEVEL SECURITY`); - - await pg.any(` - CREATE POLICY test_policy ON test_policy_table - FOR SELECT - USING (user_id = current_user) - `); - }); - - it('verify_policy should return true for existing policy', async () => { - const [result] = await pg.any( - `SELECT verify_policy('test_policy', 'test_policy_table') as verified` - ); - expect(result.verified).toBe(true); - }); - - it('verify_policy should throw for non-existent policy', async () => { - await expect( - pg.any(`SELECT verify_policy('nonexistent_policy', 'test_policy_table')`) - ).rejects.toThrow('Nonexistent policy'); - }); - }); -}); \ No newline at end of file diff --git a/packages/verify/deploy/procedures/assert_domain.sql b/packages/verify/deploy/procedures/assert_domain.sql new file mode 100644 index 000000000..885f06714 --- /dev/null +++ b/packages/verify/deploy/procedures/assert_domain.sql @@ -0,0 +1,68 @@ +-- Deploy procedures/assert_domain to pg + +BEGIN; + +-- Asserts a domain exists under exactly this name, is built on the base type it +-- is supposed to be, and still carries its constraints. +-- +-- SELECT assert_domain('public.email'::regtype); +-- SELECT assert_domain('public.email'::regtype, 'text'::regtype, _not_null => true); +-- SELECT assert_domain('public.email'::regtype, _constraints => 1); +-- +-- A domain whose CHECK constraint was dropped still exists, so the constraint +-- count is the part that catches a domain that has stopped validating anything. + +CREATE FUNCTION assert_domain ( + _domain regtype, + _base regtype DEFAULT NULL, + _not_null boolean DEFAULT NULL, + _constraints int DEFAULT NULL +) + RETURNS boolean + AS $$ +DECLARE + typ pg_catalog.pg_type%ROWTYPE; + found_constraints int; +BEGIN + SELECT + * INTO STRICT typ + FROM + pg_catalog.pg_type + WHERE + oid = _domain; + + IF typ.typtype <> 'd' THEN + RAISE EXCEPTION '% must be a domain', _domain; + END IF; + + IF _base IS NOT NULL AND typ.typbasetype <> _base THEN + RAISE EXCEPTION 'Domain % must be built on %, found %', _domain, _base, + typ.typbasetype::regtype; + END IF; + + IF _not_null IS NOT NULL AND typ.typnotnull <> _not_null THEN + RAISE EXCEPTION 'Domain % must be %', _domain, + CASE WHEN _not_null THEN 'NOT NULL' ELSE 'nullable' END; + END IF; + + IF _constraints IS NOT NULL THEN + SELECT + count(*) INTO found_constraints + FROM + pg_catalog.pg_constraint + WHERE + contypid = _domain; + + IF found_constraints <> _constraints THEN + RAISE EXCEPTION 'Domain % must carry % constraint(s), found %', _domain, + _constraints, found_constraints; + END IF; + END IF; + + RETURN TRUE; +END; +$$ +LANGUAGE 'plpgsql' +STABLE; + +COMMIT; diff --git a/packages/verify/deploy/procedures/assert_function.sql b/packages/verify/deploy/procedures/assert_function.sql new file mode 100644 index 000000000..2963adf4c --- /dev/null +++ b/packages/verify/deploy/procedures/assert_function.sql @@ -0,0 +1,70 @@ +-- Deploy procedures/assert_function to pg + +BEGIN; + +-- Asserts a function exists with exactly this signature, and that the parts of +-- its definition a caller depends on have not drifted. +-- +-- The function is named as a regprocedure so the signature is resolved by +-- Postgres rather than matched by name: a missing overload raises here, where a +-- name-only check happily passes on a different one. +-- +-- SELECT assert_function('my_schema.my_fn(uuid, text)'::regprocedure, 'boolean'::regtype); +-- +-- Every expectation is optional; NULL means the caller does not know it. + +CREATE FUNCTION assert_function ( + _function regprocedure, + _return_type regtype DEFAULT NULL, + _returns_set boolean DEFAULT NULL, + _security_definer boolean DEFAULT NULL, + _volatility text DEFAULT NULL +) + RETURNS boolean + AS $$ +DECLARE + proc pg_catalog.pg_proc%ROWTYPE; + found_volatility text; +BEGIN + SELECT + * INTO STRICT proc + FROM + pg_catalog.pg_proc + WHERE + oid = _function; + + IF _return_type IS NOT NULL AND proc.prorettype <> _return_type THEN + RAISE EXCEPTION 'Function % must return %, found %', _function, _return_type, proc.prorettype::regtype + USING HINT = 'The return type changed; callers and views built on it will break.'; + END IF; + + IF _returns_set IS NOT NULL AND proc.proretset <> _returns_set THEN + RAISE EXCEPTION 'Function % must return %', _function, + CASE WHEN _returns_set THEN 'a set' ELSE 'a single row' END; + END IF; + + IF _security_definer IS NOT NULL AND proc.prosecdef <> _security_definer THEN + RAISE EXCEPTION 'Function % must be SECURITY %', _function, + CASE WHEN _security_definer THEN 'DEFINER' ELSE 'INVOKER' END + USING HINT = 'An unintended SECURITY DEFINER runs as the owner and bypasses RLS.'; + END IF; + + IF _volatility IS NOT NULL THEN + found_volatility := CASE proc.provolatile + WHEN 'i' THEN 'IMMUTABLE' + WHEN 's' THEN 'STABLE' + ELSE 'VOLATILE' + END; + + IF found_volatility <> upper(_volatility) THEN + RAISE EXCEPTION 'Function % must be %, found %', _function, upper(_volatility), found_volatility; + END IF; + END IF; + + RETURN TRUE; +END; +$$ +LANGUAGE 'plpgsql' +STABLE; + +COMMIT; diff --git a/packages/verify/deploy/procedures/assert_function_grant.sql b/packages/verify/deploy/procedures/assert_function_grant.sql new file mode 100644 index 000000000..0257af885 --- /dev/null +++ b/packages/verify/deploy/procedures/assert_function_grant.sql @@ -0,0 +1,33 @@ +-- Deploy procedures/assert_function_grant to pg + +BEGIN; + +-- Asserts a role does (or does not) hold a privilege on a function. +-- +-- SELECT assert_function_grant('app_jobs.add_job(text)'::regprocedure, 'authenticated'); +-- +-- The function is named as a regprocedure, so a signature that no longer exists +-- raises here instead of being reported as an absent privilege. + +CREATE FUNCTION assert_function_grant ( + _function regprocedure, + _role name, + _privilege text DEFAULT 'EXECUTE', + _granted boolean DEFAULT TRUE +) + RETURNS boolean + AS $$ +BEGIN + IF pg_catalog.has_function_privilege(_role, _function, _privilege) <> _granted THEN + RAISE EXCEPTION 'Role % must % % on %', _role, + CASE WHEN _granted THEN 'hold' ELSE 'not hold' END, + _privilege, _function; + END IF; + + RETURN TRUE; +END; +$$ +LANGUAGE 'plpgsql' +STABLE; + +COMMIT; diff --git a/packages/verify/deploy/procedures/assert_index.sql b/packages/verify/deploy/procedures/assert_index.sql new file mode 100644 index 000000000..cd7da7c87 --- /dev/null +++ b/packages/verify/deploy/procedures/assert_index.sql @@ -0,0 +1,52 @@ +-- Deploy procedures/assert_index to pg + +BEGIN; + +-- Asserts an index exists, covers the table it is supposed to cover, and is +-- still unique and valid. +-- +-- SELECT assert_index('my_schema.users_email_key'::regclass, +-- 'my_schema.users'::regclass, +-- _unique => true); +-- +-- An invalid index (a failed CREATE INDEX CONCURRENTLY) exists in the catalog +-- but is ignored by the planner and enforces no uniqueness. + +CREATE FUNCTION assert_index ( + _index regclass, + _table regclass DEFAULT NULL, + _unique boolean DEFAULT NULL +) + RETURNS boolean + AS $$ +DECLARE + ind pg_catalog.pg_index%ROWTYPE; +BEGIN + SELECT + * INTO STRICT ind + FROM + pg_catalog.pg_index + WHERE + indexrelid = _index; + + IF _table IS NOT NULL AND ind.indrelid <> _table THEN + RAISE EXCEPTION 'Index % must index %, found %', _index, _table, ind.indrelid::regclass; + END IF; + + IF _unique IS NOT NULL AND ind.indisunique <> _unique THEN + RAISE EXCEPTION 'Index % must be %', _index, + CASE WHEN _unique THEN 'UNIQUE' ELSE 'non-unique' END; + END IF; + + IF NOT ind.indisvalid THEN + RAISE EXCEPTION 'Index % must be valid', _index + USING HINT = 'An invalid index is ignored by the planner and enforces nothing.'; + END IF; + + RETURN TRUE; +END; +$$ +LANGUAGE 'plpgsql' +STABLE; + +COMMIT; diff --git a/packages/verify/deploy/procedures/assert_policy.sql b/packages/verify/deploy/procedures/assert_policy.sql new file mode 100644 index 000000000..1829ba700 --- /dev/null +++ b/packages/verify/deploy/procedures/assert_policy.sql @@ -0,0 +1,81 @@ +-- Deploy procedures/assert_policy to pg + +-- requires: procedures/assert_policy_command_label + +BEGIN; + +-- Asserts an RLS policy is attached to this table and still applies to the same +-- command, with the same permissiveness and the same clauses. +-- +-- SELECT assert_policy('my_schema.docs'::regclass, 'can_select', 'SELECT', +-- _permissive => true, _has_qual => true); +-- +-- Each expectation is optional, because an ALTER POLICY action can only speak +-- for the clause it altered: the command and the permissiveness a policy was +-- created with cannot be changed by ALTER POLICY at all. + +CREATE FUNCTION assert_policy ( + _table regclass, + _policy name, + _command text DEFAULT NULL, + _permissive boolean DEFAULT NULL, + _has_qual boolean DEFAULT NULL, + _has_with_check boolean DEFAULT NULL +) + RETURNS boolean + AS $$ +DECLARE + pol pg_catalog.pg_policy%ROWTYPE; + wanted_cmd "char"; +BEGIN + SELECT + * INTO STRICT pol + FROM + pg_catalog.pg_policy + WHERE + polrelid = _table + AND polname = _policy; + + IF _command IS NOT NULL THEN + -- pg_policy.polcmd: '*' ALL, 'r' SELECT, 'a' INSERT, 'w' UPDATE, 'd' DELETE. + wanted_cmd := CASE lower(_command) + WHEN 'all' THEN '*' + WHEN 'select' THEN 'r' + WHEN 'insert' THEN 'a' + WHEN 'update' THEN 'w' + WHEN 'delete' THEN 'd' + END; + + IF wanted_cmd IS NULL THEN + RAISE EXCEPTION 'Unsupported policy command --> %', _command; + END IF; + + IF pol.polcmd <> wanted_cmd THEN + RAISE EXCEPTION 'Policy % on % must apply to %, found %', _policy, _table, + upper(_command), assert_policy_command_label (pol.polcmd); + END IF; + END IF; + + IF _permissive IS NOT NULL AND pol.polpermissive <> _permissive THEN + RAISE EXCEPTION 'Policy % on % must be %', _policy, _table, + CASE WHEN _permissive THEN 'PERMISSIVE' ELSE 'RESTRICTIVE' END + USING HINT = 'A restrictive policy turned permissive widens access instead of narrowing it.'; + END IF; + + IF _has_qual IS NOT NULL AND (pol.polqual IS NOT NULL) <> _has_qual THEN + RAISE EXCEPTION 'Policy % on % must % a USING clause', _policy, _table, + CASE WHEN _has_qual THEN 'carry' ELSE 'not carry' END; + END IF; + + IF _has_with_check IS NOT NULL AND (pol.polwithcheck IS NOT NULL) <> _has_with_check THEN + RAISE EXCEPTION 'Policy % on % must % a WITH CHECK clause', _policy, _table, + CASE WHEN _has_with_check THEN 'carry' ELSE 'not carry' END; + END IF; + + RETURN TRUE; +END; +$$ +LANGUAGE 'plpgsql' +STABLE; + +COMMIT; diff --git a/packages/verify/deploy/procedures/assert_policy_command_label.sql b/packages/verify/deploy/procedures/assert_policy_command_label.sql new file mode 100644 index 000000000..6d4518e89 --- /dev/null +++ b/packages/verify/deploy/procedures/assert_policy_command_label.sql @@ -0,0 +1,23 @@ +-- Deploy procedures/assert_policy_command_label to pg + +BEGIN; + +-- Spells a pg_policy.polcmd letter for an assertion message. + +CREATE FUNCTION assert_policy_command_label (_polcmd "char") + RETURNS text + AS $$ + SELECT + CASE _polcmd + WHEN '*' THEN 'ALL' + WHEN 'r' THEN 'SELECT' + WHEN 'a' THEN 'INSERT' + WHEN 'w' THEN 'UPDATE' + WHEN 'd' THEN 'DELETE' + ELSE format('command %L', _polcmd) + END; +$$ +LANGUAGE 'sql' +IMMUTABLE; + +COMMIT; diff --git a/packages/verify/deploy/procedures/assert_relkind_label.sql b/packages/verify/deploy/procedures/assert_relkind_label.sql new file mode 100644 index 000000000..c473b07de --- /dev/null +++ b/packages/verify/deploy/procedures/assert_relkind_label.sql @@ -0,0 +1,29 @@ +-- Deploy procedures/assert_relkind_label to pg + +BEGIN; + +-- Spells a pg_class.relkind for an assertion message, so a failure says what +-- the relation actually is instead of printing a bare letter. + +CREATE FUNCTION assert_relkind_label (_relkind "char") + RETURNS text + AS $$ + SELECT + CASE _relkind + WHEN 'r' THEN 'an ordinary table' + WHEN 'p' THEN 'a partitioned table' + WHEN 'v' THEN 'a view' + WHEN 'm' THEN 'a materialized view' + WHEN 'i' THEN 'an index' + WHEN 'I' THEN 'a partitioned index' + WHEN 'S' THEN 'a sequence' + WHEN 'f' THEN 'a foreign table' + WHEN 'c' THEN 'a composite type' + WHEN 't' THEN 'a TOAST table' + ELSE format('a relation of kind %L', _relkind) + END; +$$ +LANGUAGE 'sql' +IMMUTABLE; + +COMMIT; diff --git a/packages/verify/deploy/procedures/assert_schema.sql b/packages/verify/deploy/procedures/assert_schema.sql new file mode 100644 index 000000000..1fc0e2b55 --- /dev/null +++ b/packages/verify/deploy/procedures/assert_schema.sql @@ -0,0 +1,37 @@ +-- Deploy procedures/assert_schema to pg + +BEGIN; + +-- Asserts a schema exists. +-- +-- SELECT assert_schema('my_schema'::regnamespace); +-- +-- The regnamespace cast is the assertion: a missing schema fails while +-- resolving the argument, so the name is a reference the parser owns rather +-- than opaque text a name-only check compares. The body catches the schema +-- being dropped between planning and execution. + +CREATE FUNCTION assert_schema ( + _schema regnamespace +) + RETURNS boolean + AS $$ +BEGIN + PERFORM + 1 + FROM + pg_catalog.pg_namespace + WHERE + oid = _schema; + + IF NOT FOUND THEN + RAISE EXCEPTION 'Nonexistent schema --> %', _schema; + END IF; + + RETURN TRUE; +END; +$$ +LANGUAGE 'plpgsql' +STABLE; + +COMMIT; diff --git a/packages/verify/deploy/procedures/assert_table.sql b/packages/verify/deploy/procedures/assert_table.sql new file mode 100644 index 000000000..3a839f01a --- /dev/null +++ b/packages/verify/deploy/procedures/assert_table.sql @@ -0,0 +1,53 @@ +-- Deploy procedures/assert_table to pg + +-- requires: procedures/assert_relkind_label + +BEGIN; + +-- Asserts a relation exists under exactly this schema-qualified name and is the +-- kind of table it is supposed to be. +-- +-- SELECT assert_table('my_schema.my_table'::regclass); +-- SELECT assert_table('my_schema.events'::regclass, _partitioned => true); +-- +-- The regclass cast resolves the name, so a table replaced by a view of the +-- same name fails here rather than passing a name-only check. + +CREATE FUNCTION assert_table ( + _table regclass, + _partitioned boolean DEFAULT false, + _is_partition boolean DEFAULT NULL +) + RETURNS boolean + AS $$ +DECLARE + rel pg_catalog.pg_class%ROWTYPE; + -- plpgsql reads an IF condition up to the first THEN, so a CASE cannot be + -- inlined there. + wanted_kind "char" = CASE WHEN _partitioned THEN 'p' ELSE 'r' END; +BEGIN + SELECT + * INTO STRICT rel + FROM + pg_catalog.pg_class + WHERE + oid = _table; + + IF rel.relkind <> wanted_kind THEN + RAISE EXCEPTION 'Relation % must be %, found %', _table, + CASE WHEN _partitioned THEN 'a partitioned table' ELSE 'an ordinary table' END, + assert_relkind_label (rel.relkind); + END IF; + + IF _is_partition IS NOT NULL AND rel.relispartition <> _is_partition THEN + RAISE EXCEPTION 'Table % must % a partition of another table', _table, + CASE WHEN _is_partition THEN 'be' ELSE 'not be' END; + END IF; + + RETURN TRUE; +END; +$$ +LANGUAGE 'plpgsql' +STABLE; + +COMMIT; diff --git a/packages/verify/deploy/procedures/assert_table_grant.sql b/packages/verify/deploy/procedures/assert_table_grant.sql new file mode 100644 index 000000000..bad8ace9d --- /dev/null +++ b/packages/verify/deploy/procedures/assert_table_grant.sql @@ -0,0 +1,63 @@ +-- Deploy procedures/assert_table_grant to pg + +BEGIN; + +-- Asserts a role does (or does not) hold a privilege on a relation. +-- +-- SELECT assert_table_grant('my_schema.users'::regclass, 'authenticated', 'SELECT'); +-- SELECT assert_table_grant('my_schema.users'::regclass, 'authenticated', 'INSERT', +-- ARRAY['display_name', 'username']); +-- +-- Columns matter: GRANT INSERT (display_name) is a column-scoped privilege, and +-- the role holds no table-wide INSERT at all. A check that reads +-- information_schema.role_table_grants -- which lists table-wide grants only -- +-- reports that as missing, so every column-scoped grant failed its verify. +-- has_column_privilege answers the question actually being asked, and both +-- forms resolve inherited and PUBLIC grants the way a client would experience +-- them. + +CREATE FUNCTION assert_table_grant ( + _relation regclass, + _role name, + _privilege text, + _columns text[] DEFAULT NULL, + _granted boolean DEFAULT TRUE +) + RETURNS boolean + AS $$ +DECLARE + col text; + offending text[] = ARRAY[]::text[]; + held boolean; +BEGIN + IF _columns IS NULL OR cardinality(_columns) = 0 THEN + held = pg_catalog.has_table_privilege(_role, _relation, _privilege); + + IF held <> _granted THEN + RAISE EXCEPTION 'Role % must % % on %', _role, + CASE WHEN _granted THEN 'hold' ELSE 'not hold' END, + _privilege, _relation; + END IF; + + RETURN TRUE; + END IF; + + FOREACH col IN ARRAY _columns LOOP + IF pg_catalog.has_column_privilege(_role, _relation, col, _privilege) <> _granted THEN + offending = offending || col; + END IF; + END LOOP; + + IF cardinality(offending) > 0 THEN + RAISE EXCEPTION 'Role % must % % on %.(%)', _role, + CASE WHEN _granted THEN 'hold' ELSE 'not hold' END, + _privilege, _relation, array_to_string(offending, ', '); + END IF; + + RETURN TRUE; +END; +$$ +LANGUAGE 'plpgsql' +STABLE; + +COMMIT; diff --git a/packages/verify/deploy/procedures/assert_table_security.sql b/packages/verify/deploy/procedures/assert_table_security.sql new file mode 100644 index 000000000..0a09d9665 --- /dev/null +++ b/packages/verify/deploy/procedures/assert_table_security.sql @@ -0,0 +1,47 @@ +-- Deploy procedures/assert_table_security to pg + +BEGIN; + +-- Asserts row level security is enabled on a relation, and optionally that it +-- is forced for the table owner too. +-- +-- SELECT assert_table_security('my_schema.users'::regclass); +-- SELECT assert_table_security('my_schema.users'::regclass, _forced => true); +-- +-- Enabling RLS without a policy denies everything, and forcing it changes who +-- the policies apply to, so both are facts a caller depends on. + +CREATE FUNCTION assert_table_security ( + _table regclass, + _enabled boolean DEFAULT TRUE, + _forced boolean DEFAULT NULL +) + RETURNS boolean + AS $$ +DECLARE + rel pg_catalog.pg_class%ROWTYPE; +BEGIN + SELECT + * INTO STRICT rel + FROM + pg_catalog.pg_class + WHERE + oid = _table; + + IF rel.relrowsecurity <> _enabled THEN + RAISE EXCEPTION 'Row level security on % must be %', _table, + CASE WHEN _enabled THEN 'enabled' ELSE 'disabled' END; + END IF; + + IF _forced IS NOT NULL AND rel.relforcerowsecurity <> _forced THEN + RAISE EXCEPTION 'Row level security on % must be % for the owner', _table, + CASE WHEN _forced THEN 'forced' ELSE 'not forced' END; + END IF; + + RETURN TRUE; +END; +$$ +LANGUAGE 'plpgsql' +STABLE; + +COMMIT; diff --git a/packages/verify/deploy/procedures/assert_trigger.sql b/packages/verify/deploy/procedures/assert_trigger.sql new file mode 100644 index 000000000..1bab5d462 --- /dev/null +++ b/packages/verify/deploy/procedures/assert_trigger.sql @@ -0,0 +1,59 @@ +-- Deploy procedures/assert_trigger to pg + +-- requires: procedures/assert_trigger_type_label + +BEGIN; + +-- Asserts a trigger is attached to this table, calls the function it was +-- created with, fires with the same timing/events/level, and is enabled. +-- +-- SELECT assert_trigger('my_schema.users'::regclass, 'stamps', +-- 'stamps.timestamps'::regproc, 23); +-- +-- A trigger name is unique per table, not per schema, so the table is part of +-- the identity rather than context. _tgtype is the pg_trigger bitmask: +-- BEFORE 2, INSTEAD OF 64, AFTER 0; INSERT 4, DELETE 8, UPDATE 16, +-- TRUNCATE 32; FOR EACH ROW 1. + +CREATE FUNCTION assert_trigger ( + _table regclass, + _trigger name, + _function regproc DEFAULT NULL, + _tgtype int DEFAULT NULL, + _enabled boolean DEFAULT true +) + RETURNS boolean + AS $$ +DECLARE + trg pg_catalog.pg_trigger%ROWTYPE; +BEGIN + SELECT + * INTO STRICT trg + FROM + pg_catalog.pg_trigger + WHERE + tgrelid = _table + AND tgname = _trigger; + + IF _function IS NOT NULL AND trg.tgfoid <> _function THEN + RAISE EXCEPTION 'Trigger % on % must call %, found %', _trigger, _table, _function, trg.tgfoid::regproc; + END IF; + + IF _tgtype IS NOT NULL AND trg.tgtype <> _tgtype THEN + RAISE EXCEPTION 'Trigger % on % must be %, found %', _trigger, _table, + assert_trigger_type_label (_tgtype), assert_trigger_type_label (trg.tgtype); + END IF; + + IF _enabled IS NOT NULL AND (trg.tgenabled <> 'D') <> _enabled THEN + RAISE EXCEPTION 'Trigger % on % must be %', _trigger, _table, + CASE WHEN _enabled THEN 'enabled' ELSE 'disabled' END + USING HINT = 'ALTER TABLE ... DISABLE TRIGGER leaves the catalog row in place.'; + END IF; + + RETURN TRUE; +END; +$$ +LANGUAGE 'plpgsql' +STABLE; + +COMMIT; diff --git a/packages/verify/deploy/procedures/assert_trigger_type_label.sql b/packages/verify/deploy/procedures/assert_trigger_type_label.sql new file mode 100644 index 000000000..a07f4d5be --- /dev/null +++ b/packages/verify/deploy/procedures/assert_trigger_type_label.sql @@ -0,0 +1,44 @@ +-- Deploy procedures/assert_trigger_type_label to pg + +BEGIN; + +-- Spells a pg_trigger.tgtype bitmask the way CREATE TRIGGER spells it, so an +-- assertion failure reads 'BEFORE INSERT OR UPDATE FOR EACH ROW' rather than +-- comparing two integers. + +CREATE FUNCTION assert_trigger_type_label (_tgtype int) + RETURNS text + AS $$ +DECLARE + events text[] = ARRAY[]::text[]; +BEGIN + IF (_tgtype & 4) = 4 THEN + events := array_append(events, 'INSERT'); + END IF; + + IF (_tgtype & 8) = 8 THEN + events := array_append(events, 'DELETE'); + END IF; + + IF (_tgtype & 16) = 16 THEN + events := array_append(events, 'UPDATE'); + END IF; + + IF (_tgtype & 32) = 32 THEN + events := array_append(events, 'TRUNCATE'); + END IF; + + RETURN format('%s %s FOR EACH %s', + CASE + WHEN (_tgtype & 64) = 64 THEN 'INSTEAD OF' + WHEN (_tgtype & 2) = 2 THEN 'BEFORE' + ELSE 'AFTER' + END, + array_to_string(events, ' OR '), + CASE WHEN (_tgtype & 1) = 1 THEN 'ROW' ELSE 'STATEMENT' END); +END; +$$ +LANGUAGE 'plpgsql' +IMMUTABLE; + +COMMIT; diff --git a/packages/verify/deploy/procedures/assert_type.sql b/packages/verify/deploy/procedures/assert_type.sql new file mode 100644 index 000000000..bca4ded92 --- /dev/null +++ b/packages/verify/deploy/procedures/assert_type.sql @@ -0,0 +1,45 @@ +-- Deploy procedures/assert_type to pg + +BEGIN; + +-- Asserts a type exists under exactly this name and is the kind of type it is +-- supposed to be. +-- +-- SELECT assert_type('public.origin'::regtype); +-- SELECT assert_type('public.rgb'::regtype, _kind => 'e'); +-- +-- The identity is a regtype, so a type that was renamed or dropped raises at +-- resolution time rather than being reported as a missing row. + +CREATE FUNCTION assert_type ( + _type regtype, + _kind "char" DEFAULT NULL +) + RETURNS boolean + AS $$ +DECLARE + typ pg_catalog.pg_type%ROWTYPE; +BEGIN + SELECT + * INTO STRICT typ + FROM + pg_catalog.pg_type + WHERE + oid = _type; + + IF typ.typtype = 'd' THEN + RAISE EXCEPTION '% is a domain', _type + USING HINT = 'Use assert_domain for a domain.'; + END IF; + + IF _kind IS NOT NULL AND typ.typtype <> _kind THEN + RAISE EXCEPTION 'Type % must be typtype %, found %', _type, _kind, typ.typtype; + END IF; + + RETURN TRUE; +END; +$$ +LANGUAGE 'plpgsql' +STABLE; + +COMMIT; diff --git a/packages/verify/deploy/procedures/assert_view.sql b/packages/verify/deploy/procedures/assert_view.sql new file mode 100644 index 000000000..e20fb6c48 --- /dev/null +++ b/packages/verify/deploy/procedures/assert_view.sql @@ -0,0 +1,67 @@ +-- Deploy procedures/assert_view to pg + +-- requires: procedures/assert_relkind_label + +BEGIN; + +-- Asserts a view exists under exactly this name, is the kind of view it is +-- supposed to be, and still reads with the security context it was created +-- with. +-- +-- SELECT assert_view('my_schema.my_view'::regclass); +-- SELECT assert_view('my_schema.my_matview'::regclass, _materialized => true); +-- +-- security_invoker is the security-relevant half: a view that silently loses it +-- reads its underlying tables with the definer's RLS. + +CREATE FUNCTION assert_view ( + _view regclass, + _materialized boolean DEFAULT false, + _security_invoker boolean DEFAULT NULL +) + RETURNS boolean + AS $$ +DECLARE + rel pg_catalog.pg_class%ROWTYPE; + invoker boolean; + -- plpgsql reads an IF condition up to the first THEN, so a CASE cannot be + -- inlined there. + wanted_kind "char" = CASE WHEN _materialized THEN 'm' ELSE 'v' END; +BEGIN + SELECT + * INTO STRICT rel + FROM + pg_catalog.pg_class + WHERE + oid = _view; + + IF rel.relkind <> wanted_kind THEN + RAISE EXCEPTION 'Relation % must be %, found %', _view, + CASE WHEN _materialized THEN 'a materialized view' ELSE 'a view' END, + assert_relkind_label (rel.relkind); + END IF; + + IF _security_invoker IS NOT NULL THEN + IF _materialized THEN + RAISE EXCEPTION 'Materialized view % cannot carry security_invoker', _view + USING HINT = 'security_invoker is an ordinary-view reloption.'; + END IF; + + -- reloptions is NULL for a view carrying no options at all, and + -- `= ANY (NULL)` is NULL rather than false. + invoker := coalesce('security_invoker=true' = ANY (rel.reloptions), false); + + IF invoker <> _security_invoker THEN + RAISE EXCEPTION 'View % must be a % view', _view, + CASE WHEN _security_invoker THEN 'security_invoker' ELSE 'security definer' END + USING HINT = 'A view that loses security_invoker reads its tables with the owner''s RLS.'; + END IF; + END IF; + + RETURN TRUE; +END; +$$ +LANGUAGE 'plpgsql' +STABLE; + +COMMIT; diff --git a/packages/verify/deploy/procedures/assert_view_rule.sql b/packages/verify/deploy/procedures/assert_view_rule.sql new file mode 100644 index 000000000..5694af125 --- /dev/null +++ b/packages/verify/deploy/procedures/assert_view_rule.sql @@ -0,0 +1,53 @@ +-- Deploy procedures/assert_view_rule to pg + +BEGIN; + +-- Asserts a rewrite rule is attached to this view and still fires on the event +-- it was created for. +-- +-- SELECT assert_view_rule('my_schema.my_view'::regclass, '_insert', 'INSERT'); + +CREATE FUNCTION assert_view_rule ( + _view regclass, + _rule name, + _event text DEFAULT NULL +) + RETURNS boolean + AS $$ +DECLARE + found_event "char"; + wanted_event "char"; +BEGIN + SELECT + ev_type INTO STRICT found_event + FROM + pg_catalog.pg_rewrite + WHERE + ev_class = _view + AND rulename = _rule; + + IF _event IS NOT NULL THEN + -- pg_rewrite.ev_type: 1 SELECT, 2 UPDATE, 3 INSERT, 4 DELETE. + wanted_event := CASE lower(_event) + WHEN 'select' THEN '1' + WHEN 'update' THEN '2' + WHEN 'insert' THEN '3' + WHEN 'delete' THEN '4' + END; + + IF wanted_event IS NULL THEN + RAISE EXCEPTION 'Unsupported rule event --> %', _event; + END IF; + + IF found_event <> wanted_event THEN + RAISE EXCEPTION 'Rule % on % must fire on %', _rule, _view, upper(_event); + END IF; + END IF; + + RETURN TRUE; +END; +$$ +LANGUAGE 'plpgsql' +STABLE; + +COMMIT; diff --git a/packages/verify/deploy/procedures/get_entity_from_str.sql b/packages/verify/deploy/procedures/get_entity_from_str.sql deleted file mode 100644 index 1c29a05a2..000000000 --- a/packages/verify/deploy/procedures/get_entity_from_str.sql +++ /dev/null @@ -1,21 +0,0 @@ --- Deploy procedures/get_entity_from_str to pg - -BEGIN; -CREATE FUNCTION get_entity_from_str (qualified_name text) - RETURNS text - AS $$ -DECLARE - parts text[]; -BEGIN - SELECT - parse_ident(qualified_name) INTO parts; - IF cardinality(parts) > 1 THEN - RETURN parts[2]; - ELSE - RETURN parts[1]; - END IF; -END; -$$ -LANGUAGE plpgsql -STRICT; -COMMIT; diff --git a/packages/verify/deploy/procedures/get_schema_from_str.sql b/packages/verify/deploy/procedures/get_schema_from_str.sql deleted file mode 100644 index 5a833a19d..000000000 --- a/packages/verify/deploy/procedures/get_schema_from_str.sql +++ /dev/null @@ -1,21 +0,0 @@ --- Deploy procedures/get_schema_from_str to pg -BEGIN; -CREATE FUNCTION get_schema_from_str (qualified_name text) - RETURNS text - AS $$ -DECLARE - parts text[]; -BEGIN - SELECT - parse_ident(qualified_name) INTO parts; - IF cardinality(parts) > 1 THEN - RETURN parts[1]; - ELSE - RETURN 'public'; - END IF; -END; -$$ -LANGUAGE plpgsql -STRICT; -COMMIT; - diff --git a/packages/verify/deploy/procedures/list_indexes.sql b/packages/verify/deploy/procedures/list_indexes.sql deleted file mode 100644 index 9e36f6ff9..000000000 --- a/packages/verify/deploy/procedures/list_indexes.sql +++ /dev/null @@ -1,29 +0,0 @@ --- Deploy procedures/list_indexes to pg --- requires: procedures/get_entity_from_str --- requires: procedures/get_schema_from_str - -BEGIN; - -CREATE FUNCTION list_indexes (_table text, _index text) - RETURNS TABLE (schema_name text, table_name text, index_name text) -AS $$ -SELECT - n.nspname::text AS schema_name, - t.relname::text AS table_name, - i.relname::text AS index_name -FROM - pg_class t, - pg_class i, - pg_index ix, - pg_catalog.pg_namespace n -WHERE - t.oid = ix.indrelid - AND i.oid = ix.indexrelid - AND n.oid = i.relnamespace - AND n.nspname = get_schema_from_str(_table) - AND i.relname = _index - AND t.relname = get_entity_from_str(_table); -$$ -LANGUAGE 'sql' IMMUTABLE; - -COMMIT; diff --git a/packages/verify/deploy/procedures/list_memberships.sql b/packages/verify/deploy/procedures/list_memberships.sql deleted file mode 100644 index ad3d56685..000000000 --- a/packages/verify/deploy/procedures/list_memberships.sql +++ /dev/null @@ -1,31 +0,0 @@ --- Deploy procedures/list_memberships to pg - -BEGIN; - -CREATE FUNCTION list_memberships (_user text) - RETURNS TABLE (rolname text) -AS $$ WITH RECURSIVE cte AS ( - SELECT - oid - FROM - pg_roles - WHERE - rolname = _user - UNION ALL - SELECT - m.roleid - FROM - cte - JOIN pg_auth_members m ON m.member = cte.oid -) -SELECT - pg_roles.rolname::text AS rolname -FROM - cte c, - pg_roles -WHERE - pg_roles.oid = c.oid; -$$ -LANGUAGE 'sql' IMMUTABLE; - -COMMIT; diff --git a/packages/verify/deploy/procedures/verify_constraint.sql b/packages/verify/deploy/procedures/verify_constraint.sql deleted file mode 100644 index 048231dbc..000000000 --- a/packages/verify/deploy/procedures/verify_constraint.sql +++ /dev/null @@ -1,27 +0,0 @@ --- Deploy procedures/verify_constraint to pg -BEGIN; --- https://stackoverflow.com/questions/20087259/how-to-find-whether-unique-key-constraint-exists-for-given-columns -CREATE FUNCTION verify_constraint (_table text, _name text) - RETURNS boolean - AS $$ -BEGIN - IF EXISTS ( - SELECT - c.conname, - pg_get_constraintdef(c.oid) - FROM - pg_constraint c - WHERE - conname = _name - AND c.conrelid = _table::regclass) THEN - RETURN TRUE; -ELSE - RAISE EXCEPTION 'Nonexistent constraint --> %', _name - USING HINT = 'Please check'; -END IF; -END; -$$ -LANGUAGE 'plpgsql' -IMMUTABLE; -COMMIT; - diff --git a/packages/verify/deploy/procedures/verify_domain.sql b/packages/verify/deploy/procedures/verify_domain.sql deleted file mode 100644 index df3c4c35d..000000000 --- a/packages/verify/deploy/procedures/verify_domain.sql +++ /dev/null @@ -1,32 +0,0 @@ --- Deploy procedures/verify_domain to pg - --- requires: procedures/get_entity_from_str --- requires: procedures/get_schema_from_str - -BEGIN; -CREATE FUNCTION verify_domain (_type text) - RETURNS boolean - AS $$ -BEGIN - IF EXISTS ( - SELECT - pg_type.typname, - n.nspname - FROM - pg_type, - pg_catalog.pg_namespace n - WHERE - typtype = 'd' - AND typname = get_entity_from_str (_type) - AND nspname = get_schema_from_str (_type)) THEN - RETURN TRUE; -ELSE - RAISE EXCEPTION 'Nonexistent type --> %', _type - USING HINT = 'Please check'; -END IF; -END; -$$ -LANGUAGE 'plpgsql' -IMMUTABLE; -COMMIT; - diff --git a/packages/verify/deploy/procedures/verify_extension.sql b/packages/verify/deploy/procedures/verify_extension.sql deleted file mode 100644 index 08caacfb9..000000000 --- a/packages/verify/deploy/procedures/verify_extension.sql +++ /dev/null @@ -1,24 +0,0 @@ --- Deploy procedures/verify_extension to pg -BEGIN; -CREATE FUNCTION verify_extension (_extname text) - RETURNS boolean - AS $$ -BEGIN - IF EXISTS ( - SELECT - 1 - FROM - pg_available_extensions - WHERE - name = _extname) THEN - RETURN TRUE; -ELSE - RAISE EXCEPTION 'Nonexistent extension --> %', _extname - USING HINT = 'Please check'; -END IF; -END; -$$ -LANGUAGE 'plpgsql' -IMMUTABLE; -COMMIT; - diff --git a/packages/verify/deploy/procedures/verify_function.sql b/packages/verify/deploy/procedures/verify_function.sql deleted file mode 100644 index b74eaf423..000000000 --- a/packages/verify/deploy/procedures/verify_function.sql +++ /dev/null @@ -1,50 +0,0 @@ --- Deploy procedures/verify_function to pg - --- requires: procedures/get_entity_from_str --- requires: procedures/get_schema_from_str - -BEGIN; -CREATE FUNCTION verify_function (_name text, _user text DEFAULT NULL) - RETURNS boolean - AS $$ -DECLARE - check_user text; - func_oid oid; -BEGIN - IF (_user IS NOT NULL) THEN - check_user = _user; - ELSE - check_user = CURRENT_USER; - END IF; - IF position('(' IN _name) > 0 THEN - func_oid = to_regprocedure(_name); - IF func_oid IS NULL THEN - RAISE EXCEPTION 'Nonexistent function --> %', _name - USING HINT = 'Please check'; - END IF; - IF has_function_privilege(check_user, func_oid, 'execute') THEN - RETURN TRUE; - ELSE - RAISE EXCEPTION 'Nonexistent function --> %', _name - USING HINT = 'Please check'; - END IF; - END IF; - IF EXISTS ( - SELECT - has_function_privilege(check_user, p.oid, 'execute') - FROM - pg_catalog.pg_proc p - JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace - WHERE - n.nspname = get_schema_from_str (_name) - AND p.proname = get_entity_from_str (_name)) THEN - RETURN TRUE; -ELSE - RAISE EXCEPTION 'Nonexistent function --> %', _name - USING HINT = 'Please check'; -END IF; -END; -$$ -LANGUAGE 'plpgsql' -IMMUTABLE; -COMMIT; diff --git a/packages/verify/deploy/procedures/verify_index.sql b/packages/verify/deploy/procedures/verify_index.sql deleted file mode 100644 index f073ea996..000000000 --- a/packages/verify/deploy/procedures/verify_index.sql +++ /dev/null @@ -1,23 +0,0 @@ --- Deploy procedures/verify_index to pg - --- requires: procedures/list_indexes - -BEGIN; -CREATE FUNCTION verify_index (_table text, _index text) - RETURNS boolean - AS $$ -BEGIN - IF EXISTS ( - SELECT - list_indexes (_table, _index)) THEN - RETURN TRUE; -ELSE - RAISE EXCEPTION 'Nonexistent index --> %', _index - USING HINT = 'Please check'; -END IF; -END; -$$ -LANGUAGE 'plpgsql' -IMMUTABLE; -COMMIT; - diff --git a/packages/verify/deploy/procedures/verify_membership.sql b/packages/verify/deploy/procedures/verify_membership.sql deleted file mode 100644 index b8edc2f48..000000000 --- a/packages/verify/deploy/procedures/verify_membership.sql +++ /dev/null @@ -1,27 +0,0 @@ --- Deploy procedures/verify_membership to pg - --- requires: procedures/list_memberships - -BEGIN; -CREATE FUNCTION verify_membership (_user text, _role text) - RETURNS boolean - AS $$ -BEGIN - IF EXISTS ( - SELECT - 1 - FROM - list_memberships (_user) - WHERE - rolname = _role) THEN - RETURN TRUE; -ELSE - RAISE EXCEPTION 'Nonexistent member --> %', _user - USING HINT = 'Please check'; -END IF; -END; -$$ -LANGUAGE 'plpgsql' -IMMUTABLE; -COMMIT; - diff --git a/packages/verify/deploy/procedures/verify_policy.sql b/packages/verify/deploy/procedures/verify_policy.sql deleted file mode 100644 index 4b37f5360..000000000 --- a/packages/verify/deploy/procedures/verify_policy.sql +++ /dev/null @@ -1,35 +0,0 @@ --- Deploy procedures/verify_policy to pg - --- requires: procedures/get_entity_from_str --- requires: procedures/get_schema_from_str - - -BEGIN; - -CREATE FUNCTION verify_policy (_policy text, _table text) - RETURNS boolean - AS $$ -BEGIN - IF EXISTS ( - SELECT - 1 - FROM - pg_class p - JOIN pg_catalog.pg_namespace n ON n.oid = p.relnamespace - JOIN pg_policy pol ON pol.polrelid = p.relfilenode - WHERE - pol.polname = _policy - AND relrowsecurity = 'true' - AND relname = get_entity_from_str (_table) - AND nspname = get_schema_from_str (_table)) THEN - RETURN TRUE; -ELSE - RAISE EXCEPTION 'Nonexistent policy or missing relrowsecurity --> %', _policy - USING HINT = 'Please check'; -END IF; -END; -$$ -LANGUAGE 'plpgsql' -IMMUTABLE; -COMMIT; - diff --git a/packages/verify/deploy/procedures/verify_role.sql b/packages/verify/deploy/procedures/verify_role.sql deleted file mode 100644 index 5c87261c0..000000000 --- a/packages/verify/deploy/procedures/verify_role.sql +++ /dev/null @@ -1,24 +0,0 @@ --- Deploy procedures/verify_role to pg -BEGIN; -CREATE FUNCTION verify_role (_user text) - RETURNS boolean - AS $$ -BEGIN - IF EXISTS ( - SELECT - 1 - FROM - pg_roles - WHERE - rolname = _user) THEN - RETURN TRUE; -ELSE - RAISE EXCEPTION 'Nonexistent user --> %', _user - USING HINT = 'Please check'; -END IF; -END; -$$ -LANGUAGE 'plpgsql' -IMMUTABLE; -COMMIT; - diff --git a/packages/verify/deploy/procedures/verify_schema.sql b/packages/verify/deploy/procedures/verify_schema.sql deleted file mode 100644 index 3eba268b8..000000000 --- a/packages/verify/deploy/procedures/verify_schema.sql +++ /dev/null @@ -1,24 +0,0 @@ --- Deploy procedures/verify_schema to pg -BEGIN; -CREATE FUNCTION verify_schema (_schema text) - RETURNS boolean - AS $$ -BEGIN - IF EXISTS ( - SELECT - * - FROM - pg_catalog.pg_namespace - WHERE - nspname = _schema) THEN - RETURN TRUE; -ELSE - RAISE EXCEPTION 'Nonexistent schema --> %', _schema - USING HINT = 'Please check'; -END IF; -END; -$$ -LANGUAGE 'plpgsql' -IMMUTABLE; -COMMIT; - diff --git a/packages/verify/deploy/procedures/verify_security.sql b/packages/verify/deploy/procedures/verify_security.sql deleted file mode 100644 index 21c7aadab..000000000 --- a/packages/verify/deploy/procedures/verify_security.sql +++ /dev/null @@ -1,33 +0,0 @@ --- Deploy procedures/verify_security to pg - --- requires: procedures/get_entity_from_str --- requires: procedures/get_schema_from_str - -BEGIN; -CREATE FUNCTION verify_security (_table text) - RETURNS boolean - AS $$ -BEGIN - IF EXISTS ( - SELECT - n.oid, - relname, - n.nspname - FROM - pg_class p - JOIN pg_catalog.pg_namespace n ON n.oid = p.relnamespace - WHERE - relrowsecurity = 'true' - AND relname = get_entity_from_str (_table) - AND nspname = get_schema_from_str (_table)) THEN - RETURN TRUE; -ELSE - RAISE EXCEPTION 'Nonexistent security --> %', _name - USING HINT = 'Please check'; -END IF; -END; -$$ -LANGUAGE 'plpgsql' -IMMUTABLE; -COMMIT; - diff --git a/packages/verify/deploy/procedures/verify_table.sql b/packages/verify/deploy/procedures/verify_table.sql deleted file mode 100644 index b43ee67a6..000000000 --- a/packages/verify/deploy/procedures/verify_table.sql +++ /dev/null @@ -1,30 +0,0 @@ --- Deploy procedures/verify_table to pg - --- requires: procedures/get_entity_from_str --- requires: procedures/get_schema_from_str - - -BEGIN; -CREATE FUNCTION verify_table (_table text) - RETURNS boolean - AS $$ -BEGIN - IF EXISTS ( - SELECT - * - FROM - information_schema.tables - WHERE - table_schema = get_schema_from_str (_table) - AND table_name = get_entity_from_str (_table)) THEN - RETURN TRUE; -ELSE - RAISE EXCEPTION 'Nonexistent table --> %', _table - USING HINT = 'Please check'; -END IF; -END; -$$ -LANGUAGE 'plpgsql' -IMMUTABLE; -COMMIT; - diff --git a/packages/verify/deploy/procedures/verify_table_grant.sql b/packages/verify/deploy/procedures/verify_table_grant.sql deleted file mode 100644 index 26a239d9c..000000000 --- a/packages/verify/deploy/procedures/verify_table_grant.sql +++ /dev/null @@ -1,32 +0,0 @@ --- Deploy procedures/verify_table_grant to pg - --- requires: procedures/get_entity_from_str --- requires: procedures/get_schema_from_str - -BEGIN; -CREATE FUNCTION verify_table_grant (_table text, _privilege text, _role text) - RETURNS boolean - AS $$ -BEGIN - IF EXISTS ( - SELECT - grantee, - privilege_type - FROM - information_schema.role_table_grants - WHERE - table_schema = get_schema_from_str (_table) - AND table_name = get_entity_from_str (_table) - AND privilege_type = _privilege - AND grantee = _role) THEN - RETURN TRUE; -ELSE - RAISE EXCEPTION 'Nonexistent table grant --> %', _privilege - USING HINT = 'Please check'; -END IF; -END; -$$ -LANGUAGE 'plpgsql' -IMMUTABLE; -COMMIT; - diff --git a/packages/verify/deploy/procedures/verify_trigger.sql b/packages/verify/deploy/procedures/verify_trigger.sql deleted file mode 100644 index e74fa8317..000000000 --- a/packages/verify/deploy/procedures/verify_trigger.sql +++ /dev/null @@ -1,32 +0,0 @@ --- Deploy procedures/verify_trigger to pg - --- requires: procedures/get_entity_from_str --- requires: procedures/get_schema_from_str - - -BEGIN; -CREATE FUNCTION verify_trigger (_trigger text) - RETURNS boolean - AS $$ -BEGIN - IF EXISTS ( - SELECT - pg_trigger.tgname, - n.nspname - FROM - pg_trigger, - pg_catalog.pg_namespace n - WHERE - tgname = get_entity_from_str (_trigger) - AND nspname = get_schema_from_str (_trigger)) THEN - RETURN TRUE; -ELSE - RAISE EXCEPTION 'Nonexistent trigger --> %', _trigger - USING HINT = 'Please check'; -END IF; -END; -$$ -LANGUAGE 'plpgsql' -IMMUTABLE; -COMMIT; - diff --git a/packages/verify/deploy/procedures/verify_type.sql b/packages/verify/deploy/procedures/verify_type.sql deleted file mode 100644 index 7dd1c1dc6..000000000 --- a/packages/verify/deploy/procedures/verify_type.sql +++ /dev/null @@ -1,32 +0,0 @@ --- Deploy procedures/verify_type to pg - --- requires: procedures/get_entity_from_str --- requires: procedures/get_schema_from_str - - -BEGIN; -CREATE FUNCTION verify_type (_type text) - RETURNS boolean - AS $$ -BEGIN - IF EXISTS ( - SELECT - pg_type.typname, - n.nspname - FROM - pg_type, - pg_catalog.pg_namespace n - WHERE - typname = get_entity_from_str (_type) - AND nspname = get_schema_from_str (_type)) THEN - RETURN TRUE; -ELSE - RAISE EXCEPTION 'Nonexistent type --> %', _type - USING HINT = 'Please check'; -END IF; -END; -$$ -LANGUAGE 'plpgsql' -IMMUTABLE; -COMMIT; - diff --git a/packages/verify/deploy/procedures/verify_view.sql b/packages/verify/deploy/procedures/verify_view.sql deleted file mode 100644 index 09fff01c6..000000000 --- a/packages/verify/deploy/procedures/verify_view.sql +++ /dev/null @@ -1,30 +0,0 @@ --- Deploy procedures/verify_view to pg - --- requires: procedures/get_entity_from_str --- requires: procedures/get_schema_from_str - - -BEGIN; -CREATE FUNCTION verify_view (_view text) - RETURNS boolean - AS $$ -BEGIN - IF EXISTS ( - SELECT - * - FROM - information_schema.views - WHERE - table_schema = get_schema_from_str (_view) - AND table_name = get_entity_from_str (_view)) THEN - RETURN TRUE; -ELSE - RAISE EXCEPTION 'Nonexistent view --> %', _view - USING HINT = 'Please check'; -END IF; -END; -$$ -LANGUAGE 'plpgsql' -IMMUTABLE; -COMMIT; - diff --git a/packages/verify/package.json b/packages/verify/package.json index 375520c4c..183c3a8a9 100644 --- a/packages/verify/package.json +++ b/packages/verify/package.json @@ -1,6 +1,6 @@ { "name": "@pgpm/verify", - "version": "0.36.0", + "version": "0.39.0", "description": "Verification utilities for PGPM deploy/verify/revert workflow", "author": "Dan Lynch ", "contributors": [ diff --git a/packages/verify/pgpm-verify.control b/packages/verify/pgpm-verify.control index 672bea6f6..8803250e2 100644 --- a/packages/verify/pgpm-verify.control +++ b/packages/verify/pgpm-verify.control @@ -1,6 +1,6 @@ # pgpm-verify extension comment = 'pgpm-verify extension' -default_version = '0.36.0' +default_version = '0.39.0' module_pathname = '$libdir/pgpm-verify' requires = 'plpgsql' relocatable = false diff --git a/packages/verify/pgpm.plan b/packages/verify/pgpm.plan index 0c9ea10cd..aa1a56fca 100644 --- a/packages/verify/pgpm.plan +++ b/packages/verify/pgpm.plan @@ -2,23 +2,19 @@ %project=pgpm-verify %uri=pgpm-verify -procedures/get_entity_from_str 2017-08-11T08:11:51Z skitch # add procedures/get_entity_from_str -procedures/get_schema_from_str 2017-08-11T08:11:51Z skitch # add procedures/get_schema_from_str -procedures/list_indexes [procedures/get_entity_from_str procedures/get_schema_from_str] 2017-08-11T08:11:51Z skitch # add procedures/list_indexes -procedures/list_memberships 2017-08-11T08:11:51Z skitch # add procedures/list_memberships -procedures/verify_constraint 2017-08-11T08:11:51Z skitch # add procedures/verify_constraint -procedures/verify_domain [procedures/get_entity_from_str procedures/get_schema_from_str] 2017-08-11T08:11:51Z skitch # add procedures/verify_domain -procedures/verify_extension 2017-08-11T08:11:51Z skitch # add procedures/verify_extension -procedures/verify_function [procedures/get_entity_from_str procedures/get_schema_from_str] 2017-08-11T08:11:51Z skitch # add procedures/verify_function -procedures/verify_index [procedures/list_indexes] 2017-08-11T08:11:51Z skitch # add procedures/verify_index -procedures/verify_membership [procedures/list_memberships] 2017-08-11T08:11:51Z skitch # add procedures/verify_membership -procedures/verify_policy [procedures/get_entity_from_str procedures/get_schema_from_str] 2017-08-11T08:11:51Z skitch # add procedures/verify_policy -procedures/verify_role 2017-08-11T08:11:51Z skitch # add procedures/verify_role -procedures/verify_schema 2017-08-11T08:11:51Z skitch # add procedures/verify_schema -procedures/verify_security [procedures/get_entity_from_str procedures/get_schema_from_str] 2017-08-11T08:11:51Z skitch # add procedures/verify_security -procedures/verify_table_grant [procedures/get_entity_from_str procedures/get_schema_from_str] 2017-08-11T08:11:51Z skitch # add procedures/verify_table_grant -procedures/verify_table [procedures/get_entity_from_str procedures/get_schema_from_str] 2017-08-11T08:11:51Z skitch # add procedures/verify_table -procedures/verify_trigger [procedures/get_entity_from_str procedures/get_schema_from_str] 2017-08-11T08:11:51Z skitch # add procedures/verify_trigger -procedures/verify_type [procedures/get_entity_from_str procedures/get_schema_from_str] 2017-08-11T08:11:51Z skitch # add procedures/verify_type -procedures/verify_view [procedures/get_entity_from_str procedures/get_schema_from_str] 2017-08-11T08:11:51Z skitch # add procedures/verify_view -@0.1.0 2025-08-26T23:56:48Z pgpm # verify +procedures/assert_relkind_label 2026-08-06T07:00:00Z devin # add procedures/assert_relkind_label +procedures/assert_trigger_type_label 2026-08-06T07:00:00Z devin # add procedures/assert_trigger_type_label +procedures/assert_policy_command_label 2026-08-06T07:00:00Z devin # add procedures/assert_policy_command_label +procedures/assert_function 2026-08-06T07:00:00Z devin # add procedures/assert_function +procedures/assert_table [procedures/assert_relkind_label] 2026-08-06T07:00:00Z devin # add procedures/assert_table +procedures/assert_view [procedures/assert_relkind_label] 2026-08-06T07:00:00Z devin # add procedures/assert_view +procedures/assert_view_rule 2026-08-06T07:00:00Z devin # add procedures/assert_view_rule +procedures/assert_index 2026-08-06T07:00:00Z devin # add procedures/assert_index +procedures/assert_trigger [procedures/assert_trigger_type_label] 2026-08-06T07:00:00Z devin # add procedures/assert_trigger +procedures/assert_policy [procedures/assert_policy_command_label] 2026-08-06T07:00:00Z devin # add procedures/assert_policy +procedures/assert_schema 2026-08-06T12:00:00Z devin # add procedures/assert_schema +procedures/assert_table_grant 2026-08-06T13:00:00Z devin # add procedures/assert_table_grant +procedures/assert_table_security 2026-08-06T14:00:00Z devin # add procedures/assert_table_security +procedures/assert_function_grant 2026-08-06T14:10:00Z devin # add procedures/assert_function_grant +procedures/assert_type 2026-08-06T15:00:00Z devin # add procedures/assert_type +procedures/assert_domain 2026-08-06T15:10:00Z devin # add procedures/assert_domain diff --git a/packages/verify/revert/procedures/assert_domain.sql b/packages/verify/revert/procedures/assert_domain.sql new file mode 100644 index 000000000..7c79f8955 --- /dev/null +++ b/packages/verify/revert/procedures/assert_domain.sql @@ -0,0 +1,7 @@ +-- Revert procedures/assert_domain from pg + +BEGIN; + +DROP FUNCTION assert_domain(regtype, regtype, bool, int4); + +COMMIT; diff --git a/packages/verify/revert/procedures/assert_function.sql b/packages/verify/revert/procedures/assert_function.sql new file mode 100644 index 000000000..1b3e9331d --- /dev/null +++ b/packages/verify/revert/procedures/assert_function.sql @@ -0,0 +1,7 @@ +-- Revert procedures/assert_function from pg + +BEGIN; + +DROP FUNCTION assert_function(regprocedure, regtype, bool, bool, text); + +COMMIT; diff --git a/packages/verify/revert/procedures/assert_function_grant.sql b/packages/verify/revert/procedures/assert_function_grant.sql new file mode 100644 index 000000000..95453f96b --- /dev/null +++ b/packages/verify/revert/procedures/assert_function_grant.sql @@ -0,0 +1,7 @@ +-- Revert procedures/assert_function_grant from pg + +BEGIN; + +DROP FUNCTION assert_function_grant(regprocedure, name, text, bool); + +COMMIT; diff --git a/packages/verify/revert/procedures/assert_index.sql b/packages/verify/revert/procedures/assert_index.sql new file mode 100644 index 000000000..72573abc5 --- /dev/null +++ b/packages/verify/revert/procedures/assert_index.sql @@ -0,0 +1,7 @@ +-- Revert procedures/assert_index from pg + +BEGIN; + +DROP FUNCTION assert_index(regclass, regclass, bool); + +COMMIT; diff --git a/packages/verify/revert/procedures/assert_policy.sql b/packages/verify/revert/procedures/assert_policy.sql new file mode 100644 index 000000000..dedb6f010 --- /dev/null +++ b/packages/verify/revert/procedures/assert_policy.sql @@ -0,0 +1,7 @@ +-- Revert procedures/assert_policy from pg + +BEGIN; + +DROP FUNCTION assert_policy(regclass, name, text, bool, bool, bool); + +COMMIT; diff --git a/packages/verify/revert/procedures/assert_policy_command_label.sql b/packages/verify/revert/procedures/assert_policy_command_label.sql new file mode 100644 index 000000000..bf519b60f --- /dev/null +++ b/packages/verify/revert/procedures/assert_policy_command_label.sql @@ -0,0 +1,7 @@ +-- Revert procedures/assert_policy_command_label from pg + +BEGIN; + +DROP FUNCTION assert_policy_command_label("char"); + +COMMIT; diff --git a/packages/verify/revert/procedures/assert_relkind_label.sql b/packages/verify/revert/procedures/assert_relkind_label.sql new file mode 100644 index 000000000..ec71d9b21 --- /dev/null +++ b/packages/verify/revert/procedures/assert_relkind_label.sql @@ -0,0 +1,7 @@ +-- Revert procedures/assert_relkind_label from pg + +BEGIN; + +DROP FUNCTION assert_relkind_label("char"); + +COMMIT; diff --git a/packages/verify/revert/procedures/assert_schema.sql b/packages/verify/revert/procedures/assert_schema.sql new file mode 100644 index 000000000..558e8686a --- /dev/null +++ b/packages/verify/revert/procedures/assert_schema.sql @@ -0,0 +1,7 @@ +-- Revert procedures/assert_schema from pg + +BEGIN; + +DROP FUNCTION assert_schema(regnamespace); + +COMMIT; diff --git a/packages/verify/revert/procedures/assert_table.sql b/packages/verify/revert/procedures/assert_table.sql new file mode 100644 index 000000000..b11fe1f63 --- /dev/null +++ b/packages/verify/revert/procedures/assert_table.sql @@ -0,0 +1,7 @@ +-- Revert procedures/assert_table from pg + +BEGIN; + +DROP FUNCTION assert_table(regclass, bool, bool); + +COMMIT; diff --git a/packages/verify/revert/procedures/assert_table_grant.sql b/packages/verify/revert/procedures/assert_table_grant.sql new file mode 100644 index 000000000..bea14fdfd --- /dev/null +++ b/packages/verify/revert/procedures/assert_table_grant.sql @@ -0,0 +1,7 @@ +-- Revert procedures/assert_table_grant from pg + +BEGIN; + +DROP FUNCTION assert_table_grant(regclass, name, text, text[], bool); + +COMMIT; diff --git a/packages/verify/revert/procedures/assert_table_security.sql b/packages/verify/revert/procedures/assert_table_security.sql new file mode 100644 index 000000000..30c93d5cb --- /dev/null +++ b/packages/verify/revert/procedures/assert_table_security.sql @@ -0,0 +1,7 @@ +-- Revert procedures/assert_table_security from pg + +BEGIN; + +DROP FUNCTION assert_table_security(regclass, bool, bool); + +COMMIT; diff --git a/packages/verify/revert/procedures/assert_trigger.sql b/packages/verify/revert/procedures/assert_trigger.sql new file mode 100644 index 000000000..fcb08df7c --- /dev/null +++ b/packages/verify/revert/procedures/assert_trigger.sql @@ -0,0 +1,7 @@ +-- Revert procedures/assert_trigger from pg + +BEGIN; + +DROP FUNCTION assert_trigger(regclass, name, regproc, int4, bool); + +COMMIT; diff --git a/packages/verify/revert/procedures/assert_trigger_type_label.sql b/packages/verify/revert/procedures/assert_trigger_type_label.sql new file mode 100644 index 000000000..95edaf097 --- /dev/null +++ b/packages/verify/revert/procedures/assert_trigger_type_label.sql @@ -0,0 +1,7 @@ +-- Revert procedures/assert_trigger_type_label from pg + +BEGIN; + +DROP FUNCTION assert_trigger_type_label(int4); + +COMMIT; diff --git a/packages/verify/revert/procedures/assert_type.sql b/packages/verify/revert/procedures/assert_type.sql new file mode 100644 index 000000000..283143170 --- /dev/null +++ b/packages/verify/revert/procedures/assert_type.sql @@ -0,0 +1,7 @@ +-- Revert procedures/assert_type from pg + +BEGIN; + +DROP FUNCTION assert_type(regtype, "char"); + +COMMIT; diff --git a/packages/verify/revert/procedures/assert_view.sql b/packages/verify/revert/procedures/assert_view.sql new file mode 100644 index 000000000..308e759ba --- /dev/null +++ b/packages/verify/revert/procedures/assert_view.sql @@ -0,0 +1,7 @@ +-- Revert procedures/assert_view from pg + +BEGIN; + +DROP FUNCTION assert_view(regclass, bool, bool); + +COMMIT; diff --git a/packages/verify/revert/procedures/assert_view_rule.sql b/packages/verify/revert/procedures/assert_view_rule.sql new file mode 100644 index 000000000..80e4b0485 --- /dev/null +++ b/packages/verify/revert/procedures/assert_view_rule.sql @@ -0,0 +1,7 @@ +-- Revert procedures/assert_view_rule from pg + +BEGIN; + +DROP FUNCTION assert_view_rule(regclass, name, text); + +COMMIT; diff --git a/packages/verify/revert/procedures/get_entity_from_str.sql b/packages/verify/revert/procedures/get_entity_from_str.sql deleted file mode 100644 index d8692b00c..000000000 --- a/packages/verify/revert/procedures/get_entity_from_str.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Revert pgpm-verify:procedures/get_entity_from_str from pg - -BEGIN; - -DROP FUNCTION get_entity_from_str(text); - -COMMIT; diff --git a/packages/verify/revert/procedures/get_schema_from_str.sql b/packages/verify/revert/procedures/get_schema_from_str.sql deleted file mode 100644 index 4a507ca35..000000000 --- a/packages/verify/revert/procedures/get_schema_from_str.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Revert pgpm-verify:procedures/get_schema_from_str from pg - -BEGIN; - -DROP FUNCTION get_schema_from_str(text); - -COMMIT; diff --git a/packages/verify/revert/procedures/list_indexes.sql b/packages/verify/revert/procedures/list_indexes.sql deleted file mode 100644 index 705b8033f..000000000 --- a/packages/verify/revert/procedures/list_indexes.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Revert pgpm-verify:procedures/list_indexes from pg - -BEGIN; - -DROP FUNCTION list_indexes(text, text); - -COMMIT; diff --git a/packages/verify/revert/procedures/list_memberships.sql b/packages/verify/revert/procedures/list_memberships.sql deleted file mode 100644 index e2f80557e..000000000 --- a/packages/verify/revert/procedures/list_memberships.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Revert pgpm-verify:procedures/list_memberships from pg - -BEGIN; - -DROP FUNCTION list_memberships(text); - -COMMIT; diff --git a/packages/verify/revert/procedures/verify_constraint.sql b/packages/verify/revert/procedures/verify_constraint.sql deleted file mode 100644 index 0db59385b..000000000 --- a/packages/verify/revert/procedures/verify_constraint.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Revert procedures/verify_constraint from pg - -BEGIN; - -DROP FUNCTION verify_constraint; - -COMMIT; diff --git a/packages/verify/revert/procedures/verify_domain.sql b/packages/verify/revert/procedures/verify_domain.sql deleted file mode 100644 index d7a25634b..000000000 --- a/packages/verify/revert/procedures/verify_domain.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Revert procedures/verify_domain from pg - -BEGIN; - -DROP FUNCTION public.verify_domain; - -COMMIT; diff --git a/packages/verify/revert/procedures/verify_extension.sql b/packages/verify/revert/procedures/verify_extension.sql deleted file mode 100644 index 95fb3df2d..000000000 --- a/packages/verify/revert/procedures/verify_extension.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Revert procedures/verify_extension from pg - -BEGIN; - -DROP FUNCTION public.verify_extension; - -COMMIT; diff --git a/packages/verify/revert/procedures/verify_function.sql b/packages/verify/revert/procedures/verify_function.sql deleted file mode 100644 index 457a7da02..000000000 --- a/packages/verify/revert/procedures/verify_function.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Revert procedures/verify_function from pg - -BEGIN; - -DROP FUNCTION verify_function; - -COMMIT; diff --git a/packages/verify/revert/procedures/verify_index.sql b/packages/verify/revert/procedures/verify_index.sql deleted file mode 100644 index 6ddda3469..000000000 --- a/packages/verify/revert/procedures/verify_index.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Revert procedures/verify_index from pg - -BEGIN; - -DROP FUNCTION verify_index; - -COMMIT; diff --git a/packages/verify/revert/procedures/verify_membership.sql b/packages/verify/revert/procedures/verify_membership.sql deleted file mode 100644 index 62e69dd00..000000000 --- a/packages/verify/revert/procedures/verify_membership.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Revert procedures/verify_membership from pg - -BEGIN; - -DROP FUNCTION verify_membership; - -COMMIT; diff --git a/packages/verify/revert/procedures/verify_policy.sql b/packages/verify/revert/procedures/verify_policy.sql deleted file mode 100644 index e16b7b5d2..000000000 --- a/packages/verify/revert/procedures/verify_policy.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Revert procedures/verify_policy from pg - -BEGIN; - -DROP FUNCTION verify_policy; - -COMMIT; diff --git a/packages/verify/revert/procedures/verify_role.sql b/packages/verify/revert/procedures/verify_role.sql deleted file mode 100644 index 5c649e8aa..000000000 --- a/packages/verify/revert/procedures/verify_role.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Revert procedures/verify_role from pg - -BEGIN; - -DROP FUNCTION verify_role; - -COMMIT; diff --git a/packages/verify/revert/procedures/verify_schema.sql b/packages/verify/revert/procedures/verify_schema.sql deleted file mode 100644 index c058f28eb..000000000 --- a/packages/verify/revert/procedures/verify_schema.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Revert procedures/verify_schema from pg - -BEGIN; - -DROP FUNCTION verify_schema; - -COMMIT; diff --git a/packages/verify/revert/procedures/verify_security.sql b/packages/verify/revert/procedures/verify_security.sql deleted file mode 100644 index 3b58b2fc5..000000000 --- a/packages/verify/revert/procedures/verify_security.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Revert procedures/verify_security from pg - -BEGIN; - -DROP FUNCTION verify_security; - -COMMIT; diff --git a/packages/verify/revert/procedures/verify_table.sql b/packages/verify/revert/procedures/verify_table.sql deleted file mode 100644 index d2235bdf5..000000000 --- a/packages/verify/revert/procedures/verify_table.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Revert procedures/verify_table from pg - -BEGIN; - -DROP FUNCTION verify_table; - -COMMIT; diff --git a/packages/verify/revert/procedures/verify_table_grant.sql b/packages/verify/revert/procedures/verify_table_grant.sql deleted file mode 100644 index 780e8497c..000000000 --- a/packages/verify/revert/procedures/verify_table_grant.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Revert procedures/verify_table_grant from pg - -BEGIN; - -DROP FUNCTION verify_table_grant; - -COMMIT; diff --git a/packages/verify/revert/procedures/verify_trigger.sql b/packages/verify/revert/procedures/verify_trigger.sql deleted file mode 100644 index c51d8a298..000000000 --- a/packages/verify/revert/procedures/verify_trigger.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Revert procedures/verify_trigger from pg - -BEGIN; - -DROP FUNCTION verify_trigger; - -COMMIT; diff --git a/packages/verify/revert/procedures/verify_type.sql b/packages/verify/revert/procedures/verify_type.sql deleted file mode 100644 index da4e1e653..000000000 --- a/packages/verify/revert/procedures/verify_type.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Revert procedures/verify_type from pg - -BEGIN; - -DROP FUNCTION verify_type; - -COMMIT; diff --git a/packages/verify/revert/procedures/verify_view.sql b/packages/verify/revert/procedures/verify_view.sql deleted file mode 100644 index 632e34a53..000000000 --- a/packages/verify/revert/procedures/verify_view.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Revert procedures/verify_view from pg - -BEGIN; - -DROP FUNCTION public.verify_view; - -COMMIT; diff --git a/packages/verify/sql/pgpm-verify--0.36.0.bundle.tar.gz b/packages/verify/sql/pgpm-verify--0.36.0.bundle.tar.gz deleted file mode 100644 index 673f1aeed..000000000 Binary files a/packages/verify/sql/pgpm-verify--0.36.0.bundle.tar.gz and /dev/null differ diff --git a/packages/verify/sql/pgpm-verify--0.36.0.sql b/packages/verify/sql/pgpm-verify--0.36.0.sql deleted file mode 100644 index bf6939ab6..000000000 --- a/packages/verify/sql/pgpm-verify--0.36.0.sql +++ /dev/null @@ -1,424 +0,0 @@ -\echo Use "CREATE EXTENSION pgpm-verify" to load this file. \quit -CREATE FUNCTION get_entity_from_str( - qualified_name text -) RETURNS text AS $EOFCODE$ -DECLARE - parts text[]; -BEGIN - SELECT - parse_ident(qualified_name) INTO parts; - IF cardinality(parts) > 1 THEN - RETURN parts[2]; - ELSE - RETURN parts[1]; - END IF; -END; -$EOFCODE$ LANGUAGE plpgsql STRICT; - -CREATE FUNCTION get_schema_from_str( - qualified_name text -) RETURNS text AS $EOFCODE$ -DECLARE - parts text[]; -BEGIN - SELECT - parse_ident(qualified_name) INTO parts; - IF cardinality(parts) > 1 THEN - RETURN parts[1]; - ELSE - RETURN 'public'; - END IF; -END; -$EOFCODE$ LANGUAGE plpgsql STRICT; - -CREATE FUNCTION list_indexes( - _table text, - _index text -) RETURNS TABLE ( - schema_name text, - table_name text, - index_name text -) AS $EOFCODE$ -SELECT - n.nspname::text AS schema_name, - t.relname::text AS table_name, - i.relname::text AS index_name -FROM - pg_class t, - pg_class i, - pg_index ix, - pg_catalog.pg_namespace n -WHERE - t.oid = ix.indrelid - AND i.oid = ix.indexrelid - AND n.oid = i.relnamespace - AND n.nspname = get_schema_from_str(_table) - AND i.relname = _index - AND t.relname = get_entity_from_str(_table); -$EOFCODE$ LANGUAGE sql IMMUTABLE; - -CREATE FUNCTION list_memberships( - _user text -) RETURNS TABLE ( - rolname text -) AS $EOFCODE$ WITH RECURSIVE cte AS ( - SELECT - oid - FROM - pg_roles - WHERE - rolname = _user - UNION ALL - SELECT - m.roleid - FROM - cte - JOIN pg_auth_members m ON m.member = cte.oid -) -SELECT - pg_roles.rolname::text AS rolname -FROM - cte c, - pg_roles -WHERE - pg_roles.oid = c.oid; -$EOFCODE$ LANGUAGE sql IMMUTABLE; - -CREATE FUNCTION verify_constraint( - _table text, - _name text -) RETURNS boolean AS $EOFCODE$ -BEGIN - IF EXISTS ( - SELECT - c.conname, - pg_get_constraintdef(c.oid) - FROM - pg_constraint c - WHERE - conname = _name - AND c.conrelid = _table::regclass) THEN - RETURN TRUE; -ELSE - RAISE EXCEPTION 'Nonexistent constraint --> %', _name - USING HINT = 'Please check'; -END IF; -END; -$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; - -CREATE FUNCTION verify_domain( - _type text -) RETURNS boolean AS $EOFCODE$ -BEGIN - IF EXISTS ( - SELECT - pg_type.typname, - n.nspname - FROM - pg_type, - pg_catalog.pg_namespace n - WHERE - typtype = 'd' - AND typname = get_entity_from_str (_type) - AND nspname = get_schema_from_str (_type)) THEN - RETURN TRUE; -ELSE - RAISE EXCEPTION 'Nonexistent type --> %', _type - USING HINT = 'Please check'; -END IF; -END; -$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; - -CREATE FUNCTION verify_extension( - _extname text -) RETURNS boolean AS $EOFCODE$ -BEGIN - IF EXISTS ( - SELECT - 1 - FROM - pg_available_extensions - WHERE - name = _extname) THEN - RETURN TRUE; -ELSE - RAISE EXCEPTION 'Nonexistent extension --> %', _extname - USING HINT = 'Please check'; -END IF; -END; -$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; - -CREATE FUNCTION verify_function( - _name text, - _user text DEFAULT NULL -) RETURNS boolean AS $EOFCODE$ -DECLARE - check_user text; - func_oid oid; -BEGIN - IF (_user IS NOT NULL) THEN - check_user = _user; - ELSE - check_user = CURRENT_USER; - END IF; - IF position('(' IN _name) > 0 THEN - func_oid = to_regprocedure(_name); - IF func_oid IS NULL THEN - RAISE EXCEPTION 'Nonexistent function --> %', _name - USING HINT = 'Please check'; - END IF; - IF has_function_privilege(check_user, func_oid, 'execute') THEN - RETURN TRUE; - ELSE - RAISE EXCEPTION 'Nonexistent function --> %', _name - USING HINT = 'Please check'; - END IF; - END IF; - IF EXISTS ( - SELECT - has_function_privilege(check_user, p.oid, 'execute') - FROM - pg_catalog.pg_proc p - JOIN pg_catalog.pg_namespace n ON n.oid = p.pronamespace - WHERE - n.nspname = get_schema_from_str (_name) - AND p.proname = get_entity_from_str (_name)) THEN - RETURN TRUE; -ELSE - RAISE EXCEPTION 'Nonexistent function --> %', _name - USING HINT = 'Please check'; -END IF; -END; -$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; - -CREATE FUNCTION verify_index( - _table text, - _index text -) RETURNS boolean AS $EOFCODE$ -BEGIN - IF EXISTS ( - SELECT - list_indexes (_table, _index)) THEN - RETURN TRUE; -ELSE - RAISE EXCEPTION 'Nonexistent index --> %', _index - USING HINT = 'Please check'; -END IF; -END; -$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; - -CREATE FUNCTION verify_membership( - _user text, - _role text -) RETURNS boolean AS $EOFCODE$ -BEGIN - IF EXISTS ( - SELECT - 1 - FROM - list_memberships (_user) - WHERE - rolname = _role) THEN - RETURN TRUE; -ELSE - RAISE EXCEPTION 'Nonexistent member --> %', _user - USING HINT = 'Please check'; -END IF; -END; -$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; - -CREATE FUNCTION verify_policy( - _policy text, - _table text -) RETURNS boolean AS $EOFCODE$ -BEGIN - IF EXISTS ( - SELECT - 1 - FROM - pg_class p - JOIN pg_catalog.pg_namespace n ON n.oid = p.relnamespace - JOIN pg_policy pol ON pol.polrelid = p.relfilenode - WHERE - pol.polname = _policy - AND relrowsecurity = 'true' - AND relname = get_entity_from_str (_table) - AND nspname = get_schema_from_str (_table)) THEN - RETURN TRUE; -ELSE - RAISE EXCEPTION 'Nonexistent policy or missing relrowsecurity --> %', _policy - USING HINT = 'Please check'; -END IF; -END; -$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; - -CREATE FUNCTION verify_role( - _user text -) RETURNS boolean AS $EOFCODE$ -BEGIN - IF EXISTS ( - SELECT - 1 - FROM - pg_roles - WHERE - rolname = _user) THEN - RETURN TRUE; -ELSE - RAISE EXCEPTION 'Nonexistent user --> %', _user - USING HINT = 'Please check'; -END IF; -END; -$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; - -CREATE FUNCTION verify_schema( - _schema text -) RETURNS boolean AS $EOFCODE$ -BEGIN - IF EXISTS ( - SELECT - * - FROM - pg_catalog.pg_namespace - WHERE - nspname = _schema) THEN - RETURN TRUE; -ELSE - RAISE EXCEPTION 'Nonexistent schema --> %', _schema - USING HINT = 'Please check'; -END IF; -END; -$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; - -CREATE FUNCTION verify_security( - _table text -) RETURNS boolean AS $EOFCODE$ -BEGIN - IF EXISTS ( - SELECT - n.oid, - relname, - n.nspname - FROM - pg_class p - JOIN pg_catalog.pg_namespace n ON n.oid = p.relnamespace - WHERE - relrowsecurity = 'true' - AND relname = get_entity_from_str (_table) - AND nspname = get_schema_from_str (_table)) THEN - RETURN TRUE; -ELSE - RAISE EXCEPTION 'Nonexistent security --> %', _name - USING HINT = 'Please check'; -END IF; -END; -$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; - -CREATE FUNCTION verify_table_grant( - _table text, - _privilege text, - _role text -) RETURNS boolean AS $EOFCODE$ -BEGIN - IF EXISTS ( - SELECT - grantee, - privilege_type - FROM - information_schema.role_table_grants - WHERE - table_schema = get_schema_from_str (_table) - AND table_name = get_entity_from_str (_table) - AND privilege_type = _privilege - AND grantee = _role) THEN - RETURN TRUE; -ELSE - RAISE EXCEPTION 'Nonexistent table grant --> %', _privilege - USING HINT = 'Please check'; -END IF; -END; -$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; - -CREATE FUNCTION verify_table( - _table text -) RETURNS boolean AS $EOFCODE$ -BEGIN - IF EXISTS ( - SELECT - * - FROM - information_schema.tables - WHERE - table_schema = get_schema_from_str (_table) - AND table_name = get_entity_from_str (_table)) THEN - RETURN TRUE; -ELSE - RAISE EXCEPTION 'Nonexistent table --> %', _table - USING HINT = 'Please check'; -END IF; -END; -$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; - -CREATE FUNCTION verify_trigger( - _trigger text -) RETURNS boolean AS $EOFCODE$ -BEGIN - IF EXISTS ( - SELECT - pg_trigger.tgname, - n.nspname - FROM - pg_trigger, - pg_catalog.pg_namespace n - WHERE - tgname = get_entity_from_str (_trigger) - AND nspname = get_schema_from_str (_trigger)) THEN - RETURN TRUE; -ELSE - RAISE EXCEPTION 'Nonexistent trigger --> %', _trigger - USING HINT = 'Please check'; -END IF; -END; -$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; - -CREATE FUNCTION verify_type( - _type text -) RETURNS boolean AS $EOFCODE$ -BEGIN - IF EXISTS ( - SELECT - pg_type.typname, - n.nspname - FROM - pg_type, - pg_catalog.pg_namespace n - WHERE - typname = get_entity_from_str (_type) - AND nspname = get_schema_from_str (_type)) THEN - RETURN TRUE; -ELSE - RAISE EXCEPTION 'Nonexistent type --> %', _type - USING HINT = 'Please check'; -END IF; -END; -$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; - -CREATE FUNCTION verify_view( - _view text -) RETURNS boolean AS $EOFCODE$ -BEGIN - IF EXISTS ( - SELECT - * - FROM - information_schema.views - WHERE - table_schema = get_schema_from_str (_view) - AND table_name = get_entity_from_str (_view)) THEN - RETURN TRUE; -ELSE - RAISE EXCEPTION 'Nonexistent view --> %', _view - USING HINT = 'Please check'; -END IF; -END; -$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; \ No newline at end of file diff --git a/packages/verify/sql/pgpm-verify--0.39.0.bundle.tar.gz b/packages/verify/sql/pgpm-verify--0.39.0.bundle.tar.gz new file mode 100644 index 000000000..fa4c231a2 Binary files /dev/null and b/packages/verify/sql/pgpm-verify--0.39.0.bundle.tar.gz differ diff --git a/packages/verify/sql/pgpm-verify--0.39.0.sql b/packages/verify/sql/pgpm-verify--0.39.0.sql new file mode 100644 index 000000000..c9af9abc5 --- /dev/null +++ b/packages/verify/sql/pgpm-verify--0.39.0.sql @@ -0,0 +1,545 @@ +\echo Use "CREATE EXTENSION pgpm-verify" to load this file. \quit +CREATE FUNCTION assert_relkind_label( + _relkind "char" +) RETURNS text AS $EOFCODE$ + SELECT + CASE _relkind + WHEN 'r' THEN 'an ordinary table' + WHEN 'p' THEN 'a partitioned table' + WHEN 'v' THEN 'a view' + WHEN 'm' THEN 'a materialized view' + WHEN 'i' THEN 'an index' + WHEN 'I' THEN 'a partitioned index' + WHEN 'S' THEN 'a sequence' + WHEN 'f' THEN 'a foreign table' + WHEN 'c' THEN 'a composite type' + WHEN 't' THEN 'a TOAST table' + ELSE format('a relation of kind %L', _relkind) + END; +$EOFCODE$ LANGUAGE sql IMMUTABLE; + +CREATE FUNCTION assert_trigger_type_label( + _tgtype int +) RETURNS text AS $EOFCODE$ +DECLARE + events text[] = ARRAY[]::text[]; +BEGIN + IF (_tgtype & 4) = 4 THEN + events := array_append(events, 'INSERT'); + END IF; + + IF (_tgtype & 8) = 8 THEN + events := array_append(events, 'DELETE'); + END IF; + + IF (_tgtype & 16) = 16 THEN + events := array_append(events, 'UPDATE'); + END IF; + + IF (_tgtype & 32) = 32 THEN + events := array_append(events, 'TRUNCATE'); + END IF; + + RETURN format('%s %s FOR EACH %s', + CASE + WHEN (_tgtype & 64) = 64 THEN 'INSTEAD OF' + WHEN (_tgtype & 2) = 2 THEN 'BEFORE' + ELSE 'AFTER' + END, + array_to_string(events, ' OR '), + CASE WHEN (_tgtype & 1) = 1 THEN 'ROW' ELSE 'STATEMENT' END); +END; +$EOFCODE$ LANGUAGE plpgsql IMMUTABLE; + +CREATE FUNCTION assert_policy_command_label( + _polcmd "char" +) RETURNS text AS $EOFCODE$ + SELECT + CASE _polcmd + WHEN '*' THEN 'ALL' + WHEN 'r' THEN 'SELECT' + WHEN 'a' THEN 'INSERT' + WHEN 'w' THEN 'UPDATE' + WHEN 'd' THEN 'DELETE' + ELSE format('command %L', _polcmd) + END; +$EOFCODE$ LANGUAGE sql IMMUTABLE; + +CREATE FUNCTION assert_function( + _function regprocedure, + _return_type regtype DEFAULT NULL, + _returns_set boolean DEFAULT NULL, + _security_definer boolean DEFAULT NULL, + _volatility text DEFAULT NULL +) RETURNS boolean AS $EOFCODE$ +DECLARE + proc pg_catalog.pg_proc%ROWTYPE; + found_volatility text; +BEGIN + SELECT + * INTO STRICT proc + FROM + pg_catalog.pg_proc + WHERE + oid = _function; + + IF _return_type IS NOT NULL AND proc.prorettype <> _return_type THEN + RAISE EXCEPTION 'Function % must return %, found %', _function, _return_type, proc.prorettype::regtype + USING HINT = 'The return type changed; callers and views built on it will break.'; + END IF; + + IF _returns_set IS NOT NULL AND proc.proretset <> _returns_set THEN + RAISE EXCEPTION 'Function % must return %', _function, + CASE WHEN _returns_set THEN 'a set' ELSE 'a single row' END; + END IF; + + IF _security_definer IS NOT NULL AND proc.prosecdef <> _security_definer THEN + RAISE EXCEPTION 'Function % must be SECURITY %', _function, + CASE WHEN _security_definer THEN 'DEFINER' ELSE 'INVOKER' END + USING HINT = 'An unintended SECURITY DEFINER runs as the owner and bypasses RLS.'; + END IF; + + IF _volatility IS NOT NULL THEN + found_volatility := CASE proc.provolatile + WHEN 'i' THEN 'IMMUTABLE' + WHEN 's' THEN 'STABLE' + ELSE 'VOLATILE' + END; + + IF found_volatility <> upper(_volatility) THEN + RAISE EXCEPTION 'Function % must be %, found %', _function, upper(_volatility), found_volatility; + END IF; + END IF; + + RETURN TRUE; +END; +$EOFCODE$ LANGUAGE plpgsql STABLE; + +CREATE FUNCTION assert_table( + _table regclass, + _partitioned boolean DEFAULT false, + _is_partition boolean DEFAULT NULL +) RETURNS boolean AS $EOFCODE$ +DECLARE + rel pg_catalog.pg_class%ROWTYPE; + -- plpgsql reads an IF condition up to the first THEN, so a CASE cannot be + -- inlined there. + wanted_kind "char" = CASE WHEN _partitioned THEN 'p' ELSE 'r' END; +BEGIN + SELECT + * INTO STRICT rel + FROM + pg_catalog.pg_class + WHERE + oid = _table; + + IF rel.relkind <> wanted_kind THEN + RAISE EXCEPTION 'Relation % must be %, found %', _table, + CASE WHEN _partitioned THEN 'a partitioned table' ELSE 'an ordinary table' END, + assert_relkind_label (rel.relkind); + END IF; + + IF _is_partition IS NOT NULL AND rel.relispartition <> _is_partition THEN + RAISE EXCEPTION 'Table % must % a partition of another table', _table, + CASE WHEN _is_partition THEN 'be' ELSE 'not be' END; + END IF; + + RETURN TRUE; +END; +$EOFCODE$ LANGUAGE plpgsql STABLE; + +CREATE FUNCTION assert_view( + _view regclass, + _materialized boolean DEFAULT false, + _security_invoker boolean DEFAULT NULL +) RETURNS boolean AS $EOFCODE$ +DECLARE + rel pg_catalog.pg_class%ROWTYPE; + invoker boolean; + -- plpgsql reads an IF condition up to the first THEN, so a CASE cannot be + -- inlined there. + wanted_kind "char" = CASE WHEN _materialized THEN 'm' ELSE 'v' END; +BEGIN + SELECT + * INTO STRICT rel + FROM + pg_catalog.pg_class + WHERE + oid = _view; + + IF rel.relkind <> wanted_kind THEN + RAISE EXCEPTION 'Relation % must be %, found %', _view, + CASE WHEN _materialized THEN 'a materialized view' ELSE 'a view' END, + assert_relkind_label (rel.relkind); + END IF; + + IF _security_invoker IS NOT NULL THEN + IF _materialized THEN + RAISE EXCEPTION 'Materialized view % cannot carry security_invoker', _view + USING HINT = 'security_invoker is an ordinary-view reloption.'; + END IF; + + -- reloptions is NULL for a view carrying no options at all, and + -- `= ANY (NULL)` is NULL rather than false. + invoker := coalesce('security_invoker=true' = ANY (rel.reloptions), false); + + IF invoker <> _security_invoker THEN + RAISE EXCEPTION 'View % must be a % view', _view, + CASE WHEN _security_invoker THEN 'security_invoker' ELSE 'security definer' END + USING HINT = 'A view that loses security_invoker reads its tables with the owner''s RLS.'; + END IF; + END IF; + + RETURN TRUE; +END; +$EOFCODE$ LANGUAGE plpgsql STABLE; + +CREATE FUNCTION assert_view_rule( + _view regclass, + _rule name, + _event text DEFAULT NULL +) RETURNS boolean AS $EOFCODE$ +DECLARE + found_event "char"; + wanted_event "char"; +BEGIN + SELECT + ev_type INTO STRICT found_event + FROM + pg_catalog.pg_rewrite + WHERE + ev_class = _view + AND rulename = _rule; + + IF _event IS NOT NULL THEN + -- pg_rewrite.ev_type: 1 SELECT, 2 UPDATE, 3 INSERT, 4 DELETE. + wanted_event := CASE lower(_event) + WHEN 'select' THEN '1' + WHEN 'update' THEN '2' + WHEN 'insert' THEN '3' + WHEN 'delete' THEN '4' + END; + + IF wanted_event IS NULL THEN + RAISE EXCEPTION 'Unsupported rule event --> %', _event; + END IF; + + IF found_event <> wanted_event THEN + RAISE EXCEPTION 'Rule % on % must fire on %', _rule, _view, upper(_event); + END IF; + END IF; + + RETURN TRUE; +END; +$EOFCODE$ LANGUAGE plpgsql STABLE; + +CREATE FUNCTION assert_index( + _index regclass, + _table regclass DEFAULT NULL, + _unique boolean DEFAULT NULL +) RETURNS boolean AS $EOFCODE$ +DECLARE + ind pg_catalog.pg_index%ROWTYPE; +BEGIN + SELECT + * INTO STRICT ind + FROM + pg_catalog.pg_index + WHERE + indexrelid = _index; + + IF _table IS NOT NULL AND ind.indrelid <> _table THEN + RAISE EXCEPTION 'Index % must index %, found %', _index, _table, ind.indrelid::regclass; + END IF; + + IF _unique IS NOT NULL AND ind.indisunique <> _unique THEN + RAISE EXCEPTION 'Index % must be %', _index, + CASE WHEN _unique THEN 'UNIQUE' ELSE 'non-unique' END; + END IF; + + IF NOT ind.indisvalid THEN + RAISE EXCEPTION 'Index % must be valid', _index + USING HINT = 'An invalid index is ignored by the planner and enforces nothing.'; + END IF; + + RETURN TRUE; +END; +$EOFCODE$ LANGUAGE plpgsql STABLE; + +CREATE FUNCTION assert_trigger( + _table regclass, + _trigger name, + _function regproc DEFAULT NULL, + _tgtype int DEFAULT NULL, + _enabled boolean DEFAULT true +) RETURNS boolean AS $EOFCODE$ +DECLARE + trg pg_catalog.pg_trigger%ROWTYPE; +BEGIN + SELECT + * INTO STRICT trg + FROM + pg_catalog.pg_trigger + WHERE + tgrelid = _table + AND tgname = _trigger; + + IF _function IS NOT NULL AND trg.tgfoid <> _function THEN + RAISE EXCEPTION 'Trigger % on % must call %, found %', _trigger, _table, _function, trg.tgfoid::regproc; + END IF; + + IF _tgtype IS NOT NULL AND trg.tgtype <> _tgtype THEN + RAISE EXCEPTION 'Trigger % on % must be %, found %', _trigger, _table, + assert_trigger_type_label (_tgtype), assert_trigger_type_label (trg.tgtype); + END IF; + + IF _enabled IS NOT NULL AND (trg.tgenabled <> 'D') <> _enabled THEN + RAISE EXCEPTION 'Trigger % on % must be %', _trigger, _table, + CASE WHEN _enabled THEN 'enabled' ELSE 'disabled' END + USING HINT = 'ALTER TABLE ... DISABLE TRIGGER leaves the catalog row in place.'; + END IF; + + RETURN TRUE; +END; +$EOFCODE$ LANGUAGE plpgsql STABLE; + +CREATE FUNCTION assert_policy( + _table regclass, + _policy name, + _command text DEFAULT NULL, + _permissive boolean DEFAULT NULL, + _has_qual boolean DEFAULT NULL, + _has_with_check boolean DEFAULT NULL +) RETURNS boolean AS $EOFCODE$ +DECLARE + pol pg_catalog.pg_policy%ROWTYPE; + wanted_cmd "char"; +BEGIN + SELECT + * INTO STRICT pol + FROM + pg_catalog.pg_policy + WHERE + polrelid = _table + AND polname = _policy; + + IF _command IS NOT NULL THEN + -- pg_policy.polcmd: '*' ALL, 'r' SELECT, 'a' INSERT, 'w' UPDATE, 'd' DELETE. + wanted_cmd := CASE lower(_command) + WHEN 'all' THEN '*' + WHEN 'select' THEN 'r' + WHEN 'insert' THEN 'a' + WHEN 'update' THEN 'w' + WHEN 'delete' THEN 'd' + END; + + IF wanted_cmd IS NULL THEN + RAISE EXCEPTION 'Unsupported policy command --> %', _command; + END IF; + + IF pol.polcmd <> wanted_cmd THEN + RAISE EXCEPTION 'Policy % on % must apply to %, found %', _policy, _table, + upper(_command), assert_policy_command_label (pol.polcmd); + END IF; + END IF; + + IF _permissive IS NOT NULL AND pol.polpermissive <> _permissive THEN + RAISE EXCEPTION 'Policy % on % must be %', _policy, _table, + CASE WHEN _permissive THEN 'PERMISSIVE' ELSE 'RESTRICTIVE' END + USING HINT = 'A restrictive policy turned permissive widens access instead of narrowing it.'; + END IF; + + IF _has_qual IS NOT NULL AND (pol.polqual IS NOT NULL) <> _has_qual THEN + RAISE EXCEPTION 'Policy % on % must % a USING clause', _policy, _table, + CASE WHEN _has_qual THEN 'carry' ELSE 'not carry' END; + END IF; + + IF _has_with_check IS NOT NULL AND (pol.polwithcheck IS NOT NULL) <> _has_with_check THEN + RAISE EXCEPTION 'Policy % on % must % a WITH CHECK clause', _policy, _table, + CASE WHEN _has_with_check THEN 'carry' ELSE 'not carry' END; + END IF; + + RETURN TRUE; +END; +$EOFCODE$ LANGUAGE plpgsql STABLE; + +CREATE FUNCTION assert_schema( + _schema regnamespace +) RETURNS boolean AS $EOFCODE$ +BEGIN + PERFORM + 1 + FROM + pg_catalog.pg_namespace + WHERE + oid = _schema; + + IF NOT FOUND THEN + RAISE EXCEPTION 'Nonexistent schema --> %', _schema; + END IF; + + RETURN TRUE; +END; +$EOFCODE$ LANGUAGE plpgsql STABLE; + +CREATE FUNCTION assert_table_grant( + _relation regclass, + _role name, + _privilege text, + _columns text[] DEFAULT NULL, + _granted boolean DEFAULT true +) RETURNS boolean AS $EOFCODE$ +DECLARE + col text; + offending text[] = ARRAY[]::text[]; + held boolean; +BEGIN + IF _columns IS NULL OR cardinality(_columns) = 0 THEN + held = pg_catalog.has_table_privilege(_role, _relation, _privilege); + + IF held <> _granted THEN + RAISE EXCEPTION 'Role % must % % on %', _role, + CASE WHEN _granted THEN 'hold' ELSE 'not hold' END, + _privilege, _relation; + END IF; + + RETURN TRUE; + END IF; + + FOREACH col IN ARRAY _columns LOOP + IF pg_catalog.has_column_privilege(_role, _relation, col, _privilege) <> _granted THEN + offending = offending || col; + END IF; + END LOOP; + + IF cardinality(offending) > 0 THEN + RAISE EXCEPTION 'Role % must % % on %.(%)', _role, + CASE WHEN _granted THEN 'hold' ELSE 'not hold' END, + _privilege, _relation, array_to_string(offending, ', '); + END IF; + + RETURN TRUE; +END; +$EOFCODE$ LANGUAGE plpgsql STABLE; + +CREATE FUNCTION assert_table_security( + _table regclass, + _enabled boolean DEFAULT true, + _forced boolean DEFAULT NULL +) RETURNS boolean AS $EOFCODE$ +DECLARE + rel pg_catalog.pg_class%ROWTYPE; +BEGIN + SELECT + * INTO STRICT rel + FROM + pg_catalog.pg_class + WHERE + oid = _table; + + IF rel.relrowsecurity <> _enabled THEN + RAISE EXCEPTION 'Row level security on % must be %', _table, + CASE WHEN _enabled THEN 'enabled' ELSE 'disabled' END; + END IF; + + IF _forced IS NOT NULL AND rel.relforcerowsecurity <> _forced THEN + RAISE EXCEPTION 'Row level security on % must be % for the owner', _table, + CASE WHEN _forced THEN 'forced' ELSE 'not forced' END; + END IF; + + RETURN TRUE; +END; +$EOFCODE$ LANGUAGE plpgsql STABLE; + +CREATE FUNCTION assert_function_grant( + _function regprocedure, + _role name, + _privilege text DEFAULT 'EXECUTE', + _granted boolean DEFAULT true +) RETURNS boolean AS $EOFCODE$ +BEGIN + IF pg_catalog.has_function_privilege(_role, _function, _privilege) <> _granted THEN + RAISE EXCEPTION 'Role % must % % on %', _role, + CASE WHEN _granted THEN 'hold' ELSE 'not hold' END, + _privilege, _function; + END IF; + + RETURN TRUE; +END; +$EOFCODE$ LANGUAGE plpgsql STABLE; + +CREATE FUNCTION assert_type( + _type regtype, + _kind "char" DEFAULT NULL +) RETURNS boolean AS $EOFCODE$ +DECLARE + typ pg_catalog.pg_type%ROWTYPE; +BEGIN + SELECT + * INTO STRICT typ + FROM + pg_catalog.pg_type + WHERE + oid = _type; + + IF typ.typtype = 'd' THEN + RAISE EXCEPTION '% is a domain', _type + USING HINT = 'Use assert_domain for a domain.'; + END IF; + + IF _kind IS NOT NULL AND typ.typtype <> _kind THEN + RAISE EXCEPTION 'Type % must be typtype %, found %', _type, _kind, typ.typtype; + END IF; + + RETURN TRUE; +END; +$EOFCODE$ LANGUAGE plpgsql STABLE; + +CREATE FUNCTION assert_domain( + _domain regtype, + _base regtype DEFAULT NULL, + _not_null boolean DEFAULT NULL, + _constraints int DEFAULT NULL +) RETURNS boolean AS $EOFCODE$ +DECLARE + typ pg_catalog.pg_type%ROWTYPE; + found_constraints int; +BEGIN + SELECT + * INTO STRICT typ + FROM + pg_catalog.pg_type + WHERE + oid = _domain; + + IF typ.typtype <> 'd' THEN + RAISE EXCEPTION '% must be a domain', _domain; + END IF; + + IF _base IS NOT NULL AND typ.typbasetype <> _base THEN + RAISE EXCEPTION 'Domain % must be built on %, found %', _domain, _base, + typ.typbasetype::regtype; + END IF; + + IF _not_null IS NOT NULL AND typ.typnotnull <> _not_null THEN + RAISE EXCEPTION 'Domain % must be %', _domain, + CASE WHEN _not_null THEN 'NOT NULL' ELSE 'nullable' END; + END IF; + + IF _constraints IS NOT NULL THEN + SELECT + count(*) INTO found_constraints + FROM + pg_catalog.pg_constraint + WHERE + contypid = _domain; + + IF found_constraints <> _constraints THEN + RAISE EXCEPTION 'Domain % must carry % constraint(s), found %', _domain, + _constraints, found_constraints; + END IF; + END IF; + + RETURN TRUE; +END; +$EOFCODE$ LANGUAGE plpgsql STABLE; \ No newline at end of file diff --git a/packages/verify/verify/procedures/assert_domain.sql b/packages/verify/verify/procedures/assert_domain.sql new file mode 100644 index 000000000..305d20216 --- /dev/null +++ b/packages/verify/verify/procedures/assert_domain.sql @@ -0,0 +1,7 @@ +-- Verify procedures/assert_domain on pg + +BEGIN; + +SELECT assert_function('public.assert_domain(regtype, regtype, boolean, int)'::regprocedure); + +ROLLBACK; diff --git a/packages/verify/verify/procedures/assert_function.sql b/packages/verify/verify/procedures/assert_function.sql new file mode 100644 index 000000000..5f5a764b4 --- /dev/null +++ b/packages/verify/verify/procedures/assert_function.sql @@ -0,0 +1,7 @@ +-- Verify procedures/assert_function on pg + +BEGIN; + +SELECT assert_function('assert_function(regprocedure, regtype, bool, bool, text)'::regprocedure); + +ROLLBACK; diff --git a/packages/verify/verify/procedures/assert_function_grant.sql b/packages/verify/verify/procedures/assert_function_grant.sql new file mode 100644 index 000000000..d588b2bd8 --- /dev/null +++ b/packages/verify/verify/procedures/assert_function_grant.sql @@ -0,0 +1,7 @@ +-- Verify procedures/assert_function_grant on pg + +BEGIN; + +SELECT assert_function('public.assert_function_grant(regprocedure, name, text, boolean)'::regprocedure); + +ROLLBACK; diff --git a/packages/verify/verify/procedures/assert_index.sql b/packages/verify/verify/procedures/assert_index.sql new file mode 100644 index 000000000..c387e9d32 --- /dev/null +++ b/packages/verify/verify/procedures/assert_index.sql @@ -0,0 +1,7 @@ +-- Verify procedures/assert_index on pg + +BEGIN; + +SELECT assert_function('assert_index(regclass, regclass, bool)'::regprocedure); + +ROLLBACK; diff --git a/packages/verify/verify/procedures/assert_policy.sql b/packages/verify/verify/procedures/assert_policy.sql new file mode 100644 index 000000000..e40243fb2 --- /dev/null +++ b/packages/verify/verify/procedures/assert_policy.sql @@ -0,0 +1,7 @@ +-- Verify procedures/assert_policy on pg + +BEGIN; + +SELECT assert_function('assert_policy(regclass, name, text, bool, bool, bool)'::regprocedure); + +ROLLBACK; diff --git a/packages/verify/verify/procedures/assert_policy_command_label.sql b/packages/verify/verify/procedures/assert_policy_command_label.sql new file mode 100644 index 000000000..7572d8679 --- /dev/null +++ b/packages/verify/verify/procedures/assert_policy_command_label.sql @@ -0,0 +1,7 @@ +-- Verify procedures/assert_policy_command_label on pg + +BEGIN; + +SELECT assert_function('assert_policy_command_label("char")'::regprocedure); + +ROLLBACK; diff --git a/packages/verify/verify/procedures/assert_relkind_label.sql b/packages/verify/verify/procedures/assert_relkind_label.sql new file mode 100644 index 000000000..35b09790b --- /dev/null +++ b/packages/verify/verify/procedures/assert_relkind_label.sql @@ -0,0 +1,7 @@ +-- Verify procedures/assert_relkind_label on pg + +BEGIN; + +SELECT assert_function('assert_relkind_label("char")'::regprocedure); + +ROLLBACK; diff --git a/packages/verify/verify/procedures/assert_schema.sql b/packages/verify/verify/procedures/assert_schema.sql new file mode 100644 index 000000000..2230f2ea4 --- /dev/null +++ b/packages/verify/verify/procedures/assert_schema.sql @@ -0,0 +1,7 @@ +-- Verify procedures/assert_schema on pg + +BEGIN; + +SELECT assert_function('assert_schema(regnamespace)'::regprocedure); + +ROLLBACK; diff --git a/packages/verify/verify/procedures/assert_table.sql b/packages/verify/verify/procedures/assert_table.sql new file mode 100644 index 000000000..f03a6ed9c --- /dev/null +++ b/packages/verify/verify/procedures/assert_table.sql @@ -0,0 +1,7 @@ +-- Verify procedures/assert_table on pg + +BEGIN; + +SELECT assert_function('assert_table(regclass, bool, bool)'::regprocedure); + +ROLLBACK; diff --git a/packages/verify/verify/procedures/assert_table_grant.sql b/packages/verify/verify/procedures/assert_table_grant.sql new file mode 100644 index 000000000..90f9e8edf --- /dev/null +++ b/packages/verify/verify/procedures/assert_table_grant.sql @@ -0,0 +1,7 @@ +-- Verify procedures/assert_table_grant on pg + +BEGIN; + +SELECT assert_function('assert_table_grant(regclass, name, text, text[], bool)'::regprocedure); + +ROLLBACK; diff --git a/packages/verify/verify/procedures/assert_table_security.sql b/packages/verify/verify/procedures/assert_table_security.sql new file mode 100644 index 000000000..1375727fa --- /dev/null +++ b/packages/verify/verify/procedures/assert_table_security.sql @@ -0,0 +1,7 @@ +-- Verify procedures/assert_table_security on pg + +BEGIN; + +SELECT assert_function('assert_table_security(regclass, bool, bool)'::regprocedure); + +ROLLBACK; diff --git a/packages/verify/verify/procedures/assert_trigger.sql b/packages/verify/verify/procedures/assert_trigger.sql new file mode 100644 index 000000000..e35a51be8 --- /dev/null +++ b/packages/verify/verify/procedures/assert_trigger.sql @@ -0,0 +1,7 @@ +-- Verify procedures/assert_trigger on pg + +BEGIN; + +SELECT assert_function('assert_trigger(regclass, name, regproc, int4, bool)'::regprocedure); + +ROLLBACK; diff --git a/packages/verify/verify/procedures/assert_trigger_type_label.sql b/packages/verify/verify/procedures/assert_trigger_type_label.sql new file mode 100644 index 000000000..945d9c599 --- /dev/null +++ b/packages/verify/verify/procedures/assert_trigger_type_label.sql @@ -0,0 +1,7 @@ +-- Verify procedures/assert_trigger_type_label on pg + +BEGIN; + +SELECT assert_function('assert_trigger_type_label(int4)'::regprocedure); + +ROLLBACK; diff --git a/packages/verify/verify/procedures/assert_type.sql b/packages/verify/verify/procedures/assert_type.sql new file mode 100644 index 000000000..a7de2b18f --- /dev/null +++ b/packages/verify/verify/procedures/assert_type.sql @@ -0,0 +1,7 @@ +-- Verify procedures/assert_type on pg + +BEGIN; + +SELECT assert_function('public.assert_type(regtype, "char")'::regprocedure); + +ROLLBACK; diff --git a/packages/verify/verify/procedures/assert_view.sql b/packages/verify/verify/procedures/assert_view.sql new file mode 100644 index 000000000..f06bec8d5 --- /dev/null +++ b/packages/verify/verify/procedures/assert_view.sql @@ -0,0 +1,7 @@ +-- Verify procedures/assert_view on pg + +BEGIN; + +SELECT assert_function('assert_view(regclass, bool, bool)'::regprocedure); + +ROLLBACK; diff --git a/packages/verify/verify/procedures/assert_view_rule.sql b/packages/verify/verify/procedures/assert_view_rule.sql new file mode 100644 index 000000000..12321cf08 --- /dev/null +++ b/packages/verify/verify/procedures/assert_view_rule.sql @@ -0,0 +1,7 @@ +-- Verify procedures/assert_view_rule on pg + +BEGIN; + +SELECT assert_function('assert_view_rule(regclass, name, text)'::regprocedure); + +ROLLBACK; diff --git a/packages/verify/verify/procedures/get_entity_from_str.sql b/packages/verify/verify/procedures/get_entity_from_str.sql deleted file mode 100644 index 59148db57..000000000 --- a/packages/verify/verify/procedures/get_entity_from_str.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Verify pgpm-verify:procedures/get_entity_from_str on pg - -BEGIN; - --- XXX Add verifications here. - -ROLLBACK; diff --git a/packages/verify/verify/procedures/get_schema_from_str.sql b/packages/verify/verify/procedures/get_schema_from_str.sql deleted file mode 100644 index fda49efde..000000000 --- a/packages/verify/verify/procedures/get_schema_from_str.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Verify pgpm-verify:procedures/get_schema_from_str on pg - -BEGIN; - --- XXX Add verifications here. - -ROLLBACK; diff --git a/packages/verify/verify/procedures/list_indexes.sql b/packages/verify/verify/procedures/list_indexes.sql deleted file mode 100644 index 8a11f2b23..000000000 --- a/packages/verify/verify/procedures/list_indexes.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Verify pgpm-verify:procedures/list_indexes on pg - -BEGIN; - --- XXX Add verifications here. - -ROLLBACK; diff --git a/packages/verify/verify/procedures/list_memberships.sql b/packages/verify/verify/procedures/list_memberships.sql deleted file mode 100644 index 1b00454ca..000000000 --- a/packages/verify/verify/procedures/list_memberships.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Verify pgpm-verify:procedures/list_memberships on pg - -BEGIN; - --- XXX Add verifications here. - -ROLLBACK; diff --git a/packages/verify/verify/procedures/verify_constraint.sql b/packages/verify/verify/procedures/verify_constraint.sql deleted file mode 100644 index fc98e3235..000000000 --- a/packages/verify/verify/procedures/verify_constraint.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Verify procedures/verify_constraint on pg - -BEGIN; - -SELECT verify_function ('public.verify_constraint'); - -ROLLBACK; diff --git a/packages/verify/verify/procedures/verify_domain.sql b/packages/verify/verify/procedures/verify_domain.sql deleted file mode 100644 index 44eba8964..000000000 --- a/packages/verify/verify/procedures/verify_domain.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Verify procedures/verify_domain on pg - -BEGIN; - -SELECT verify_function ('public.verify_domain'); - -ROLLBACK; diff --git a/packages/verify/verify/procedures/verify_extension.sql b/packages/verify/verify/procedures/verify_extension.sql deleted file mode 100644 index 5d1d2b81a..000000000 --- a/packages/verify/verify/procedures/verify_extension.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Verify procedures/verify_extension on pg - -BEGIN; - -SELECT verify_function ('public.verify_extension'); - -ROLLBACK; diff --git a/packages/verify/verify/procedures/verify_function.sql b/packages/verify/verify/procedures/verify_function.sql deleted file mode 100644 index 857015ea9..000000000 --- a/packages/verify/verify/procedures/verify_function.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Verify procedures/verify_function on pg - -BEGIN; - -SELECT verify_function ('public.verify_function'); - -ROLLBACK; diff --git a/packages/verify/verify/procedures/verify_index.sql b/packages/verify/verify/procedures/verify_index.sql deleted file mode 100644 index ce75113e5..000000000 --- a/packages/verify/verify/procedures/verify_index.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Verify procedures/verify_index on pg - -BEGIN; - -SELECT verify_function ('public.verify_index'); - -ROLLBACK; diff --git a/packages/verify/verify/procedures/verify_membership.sql b/packages/verify/verify/procedures/verify_membership.sql deleted file mode 100644 index 547dfc47b..000000000 --- a/packages/verify/verify/procedures/verify_membership.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Verify procedures/verify_membership on pg - -BEGIN; - -SELECT verify_function ('public.verify_membership'); - -ROLLBACK; diff --git a/packages/verify/verify/procedures/verify_policy.sql b/packages/verify/verify/procedures/verify_policy.sql deleted file mode 100644 index 36dd6648a..000000000 --- a/packages/verify/verify/procedures/verify_policy.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Verify procedures/verify_policy on pg - -BEGIN; - -SELECT verify_function ('public.verify_policy'); - -ROLLBACK; diff --git a/packages/verify/verify/procedures/verify_role.sql b/packages/verify/verify/procedures/verify_role.sql deleted file mode 100644 index 717b11791..000000000 --- a/packages/verify/verify/procedures/verify_role.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Verify procedures/verify_role on pg - -BEGIN; - -SELECT verify_function ('public.verify_role'); - -ROLLBACK; diff --git a/packages/verify/verify/procedures/verify_schema.sql b/packages/verify/verify/procedures/verify_schema.sql deleted file mode 100644 index 6420dcbbc..000000000 --- a/packages/verify/verify/procedures/verify_schema.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Verify procedures/verify_schema on pg - -BEGIN; - -SELECT verify_function ('public.verify_schema'); - -ROLLBACK; diff --git a/packages/verify/verify/procedures/verify_security.sql b/packages/verify/verify/procedures/verify_security.sql deleted file mode 100644 index a4e570a3f..000000000 --- a/packages/verify/verify/procedures/verify_security.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Verify procedures/verify_security on pg - -BEGIN; - -SELECT verify_function ('public.verify_security'); - -ROLLBACK; diff --git a/packages/verify/verify/procedures/verify_table.sql b/packages/verify/verify/procedures/verify_table.sql deleted file mode 100644 index 5a198e3da..000000000 --- a/packages/verify/verify/procedures/verify_table.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Verify procedures/verify_table on pg - -BEGIN; - -SELECT verify_function ('public.verify_table'); - -ROLLBACK; diff --git a/packages/verify/verify/procedures/verify_table_grant.sql b/packages/verify/verify/procedures/verify_table_grant.sql deleted file mode 100644 index 10efe5a20..000000000 --- a/packages/verify/verify/procedures/verify_table_grant.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Verify procedures/verify_table_grant on pg - -BEGIN; - -SELECT verify_function ('public.verify_table_grant'); - -ROLLBACK; diff --git a/packages/verify/verify/procedures/verify_trigger.sql b/packages/verify/verify/procedures/verify_trigger.sql deleted file mode 100644 index 6234bb8d7..000000000 --- a/packages/verify/verify/procedures/verify_trigger.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Verify procedures/verify_trigger on pg - -BEGIN; - -SELECT verify_function ('public.verify_trigger'); - -ROLLBACK; diff --git a/packages/verify/verify/procedures/verify_type.sql b/packages/verify/verify/procedures/verify_type.sql deleted file mode 100644 index 58eb77801..000000000 --- a/packages/verify/verify/procedures/verify_type.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Verify procedures/verify_type on pg - -BEGIN; - -SELECT verify_function ('public.verify_type'); - -ROLLBACK; diff --git a/packages/verify/verify/procedures/verify_view.sql b/packages/verify/verify/procedures/verify_view.sql deleted file mode 100644 index 3d4e4431e..000000000 --- a/packages/verify/verify/procedures/verify_view.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Verify procedures/verify_view on pg - -BEGIN; - -SELECT verify_function ('public.verify_view'); - -ROLLBACK; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index adceb20cf..8349ea3d9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -48,8 +48,8 @@ importers: specifier: ^8.2.3 version: 8.2.4(@types/node@22.19.3)(encoding@0.1.13) pgsql-test: - specifier: ^5.4.1 - version: 5.4.1 + specifier: ^5.10.6 + version: 5.10.7 pnpm-policy: specifier: ^0.2.2 version: 0.2.2 @@ -411,6 +411,9 @@ packages: 12factor-env@1.21.0: resolution: {integrity: sha512-wWaC+62wfdVW8yqvoVyGkE2s+QztejYvD8IJppVS1yB1ZCOe7H9mJ8U1bJhM0CiHKE02w3tvlFhKKXbFMXmrhw==} + 12factor-env@1.27.1: + resolution: {integrity: sha512-i8KO5QPU69ntJsjG746Yj1hWN/eFc1A+wJRUaDcCJOG6ibw2oQ1ZV37t3pkmRIy8vRJzLnLZGsKhPmgkvZdP5A==} + '@0no-co/graphql.web@1.2.0': resolution: {integrity: sha512-/1iHy9TTr63gE1YcR5idjx8UREz1s0kFhydf3bBLCXyqjhkIc6igAzTOx3zPifCwFR87tsh/4Pa9cNts6d2otw==} peerDependencies: @@ -709,6 +712,9 @@ packages: '@constructive-io/content-type-stream@2.24.0': resolution: {integrity: sha512-vgY/nFWiq2PUD3Twy85WfwTCCcWBzMeN8BdU+P6244i5s8if6vrtOjIwmQFwDlw5PS/JRvmtw0NjIEqLXgreHg==} + '@constructive-io/errors@0.10.0': + resolution: {integrity: sha512-AFMvEcT7mqHBlqTJytJVvzmGdio2ys1uG4E9d0qF5wz+sdLwBMdEl8TBbxuBieSQgZAMAUoYOynCIrbwWtwArw==} + '@constructive-io/express-context@0.18.0': resolution: {integrity: sha512-apjuKG7031WE4m3WW+6Ap2kDvUtTR0jkBumXrT/1sIoLInYg9sWe17JR7DXse84RAtw5CaWcX/v2vrWYqmqfEg==} peerDependencies: @@ -1268,57 +1274,111 @@ packages: '@octokit/types@13.10.0': resolution: {integrity: sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==} + '@pgpmjs/ast@0.11.3': + resolution: {integrity: sha512-rlkf8NwhV2Fgn3JgjVmXPxehnxZCDttu9ufwj3gNuGGoCxSQ7kSOaCSTtHyYGhssJ1sux4+3TfQc3jluurNsbQ==} + '@pgpmjs/ast@0.5.0': resolution: {integrity: sha512-68bC8OsU96ZcxnLQ/qnPIIp5FSs8VvvzuvPYz5HHUrTRwW9H8/83/KfkqAHXMvxIFMuVGcF1djr0e6rFq6ovhw==} + '@pgpmjs/bundle@0.14.3': + resolution: {integrity: sha512-pIqXuJw2ac1hffNowIaaSoc6XIqFxmXAQFNVJqi2OJpQT3+zUJI1z5XRKYPcg4AgLQ4u9YmcIv6/8nBpbpNyJg==} + '@pgpmjs/bundle@0.6.0': resolution: {integrity: sha512-4iuqU0qttaRePLIBfDifFQwTrcr9KhnkRY0nBPktxsBEgjWC7D2xIzHc1RArkZZudNM5aMKHdT6LpGS7GKD39A==} + '@pgpmjs/core@7.25.4': + resolution: {integrity: sha512-N2Wy3v+eMFKH3ziC5e1c4GymF1jpY/E9L9FKWJ/1706u61N8QGDCkaXmpPnmRaybQANu1U9NVzL/8QqS7luujg==} + '@pgpmjs/core@7.9.0': resolution: {integrity: sha512-st2B8EtBa0WVbNv6RZO8nriWaAABuvf1mVoqrVecOeFLyoH5xyDGgR1bK0WT3K+Yd2iD3iFmy5jP9wSx86/8ww==} + '@pgpmjs/diff@0.3.5': + resolution: {integrity: sha512-lfT14BABR+AD6k7OWP2M5I1aawJXpUbkobxLPn6sMbKDBfUsPTflgLSSFJFMvs3DTLeyB2GotMKRZzSmE24DXQ==} + '@pgpmjs/env@2.33.0': resolution: {integrity: sha512-/NlCXAtMd6mbp8lVsiLHo/ErahpF3Pjfz5KvGENHtFnpWIN2Mi9oQhTo8grzH6n47/qyQ36wZ1hYhY8HSigSEA==} + '@pgpmjs/env@2.41.3': + resolution: {integrity: sha512-11XEcfSMPJ0xAErusa6qo4A1+/YfqRLWM8j43M1HzLM0cnDiZyhriGyWQFZUaOOUDh6OVuIl0UJEggX8Hof0Tw==} + '@pgpmjs/export@1.8.1': resolution: {integrity: sha512-50sPsGXogwN0RiF3EHxGIk3BbWSehlTVGc1kieYW0MOtiG5aS1FNBBa1yc6wyW5VxhW1YDmVfdvpySzs0hW3qw==} '@pgpmjs/logger@2.18.0': resolution: {integrity: sha512-2Crr/s299ahmXv0fxasu8GVrV6VovQhGUtFsM4dVUjHx3mXlW8P+nfkHM2BnQxWeqWV3u25z7aXnRiWDCZ0K/Q==} + '@pgpmjs/logger@2.24.1': + resolution: {integrity: sha512-T8WxB4GBM0zAE/C8HewmA88zIIrAGDV4cQi6v8tf630lXkg2JxeCDv4Ze8gtgB7A4NLnN0R+b3AdFmJd8c/30Q==} + '@pgpmjs/migrate-client@1.5.1': resolution: {integrity: sha512-JMOryDM5FhTJZFoINv23aee/Lesbwm6Bkayn9WBi9XlKtSCo5p4DGkYd4x3mG7sYDq8phdCit511xf1P9E0Q5g==} + '@pgpmjs/naming-spec@0.3.0': + resolution: {integrity: sha512-QO7UPu+yopry1UXaKg4854vjZmLI3CXhX9BzRjvHNvzbVubEz5EjD5bcsPgG0V18utkWSp9LfRz4emcEEnjdBQ==} + '@pgpmjs/server-utils@3.19.0': resolution: {integrity: sha512-36i7q7+bzbklE1Y7+bdQJbMuBhuTRqcznmnDs5HvfM29B9gFK5TXTVO1h3B4FArpmVpYctr303PkqL/a7T+gzg==} + '@pgpmjs/server-utils@3.25.4': + resolution: {integrity: sha512-0XrttIUyjZfMd6/goaEuD1f7gyVx/tvjUAIiUXuerCceWnuP6Rpg08mVcOe56WTZrF3jnkulbbbuEpo1INRDjw==} + + '@pgpmjs/slice@0.14.6': + resolution: {integrity: sha512-ZffIlXqj5Jzsi5Ik+wmER+k98XH6qSXGMSYb+X33A6kFFYRjhsZrQUgug1EOyL/EtIX27xbw4IPrTn4kBU/5dw==} + '@pgpmjs/slice@0.5.0': resolution: {integrity: sha512-QIjKD229ip1mfCpmjxQzS4eLjNwSjcciIAvKUvf9vF8vTHAH+Ix12B3yS/o8T1if0B+BokyLNyEGG/x5cgX9Pw==} + '@pgpmjs/transform@0.23.4': + resolution: {integrity: sha512-Y5iPgI0E8VOr0AMgq+Y5W4KwAvuCAGb2lo8wwKiXAXz5YKQvoCz6kDcbOCstj31wW1XnaiYUOn3pEhB+ODPE+w==} + '@pgpmjs/transform@0.5.0': resolution: {integrity: sha512-HERnBjsMAWbi8s31/mEAJI5xYWEfvczQLxmfkmuWKONUmYv7Pvk3EvgPj6e7vukBCAP+lakPQbyb4cszO/xwNg==} + '@pgpmjs/traverse@0.11.3': + resolution: {integrity: sha512-84HuCy/JZ9P7bKB/B3s5kyMgRO7Rh0zjIVNcK6NGXajNbY200LNXEKLzATB81yNEGRMCtDfmqfxcc6Iw2nVVHw==} + '@pgpmjs/traverse@0.5.0': resolution: {integrity: sha512-Lc8oeKqjQor+mfgx3tkDDuEVxImgiKtbecKKcX6Tjs7/nD/WvK+GotF9/onV3x2KzqAuz+kWpUqBon5kKEot3g==} '@pgpmjs/types@2.40.0': resolution: {integrity: sha512-rI3DLp94jS9PhWzfAyeW0Gn+L8HrwKvjTQHd8X7uQsc9bCCl0TwxKaPiUFJHdgzYSBJltjCWsxWNZ1wp2R0Org==} + '@pgpmjs/types@2.50.3': + resolution: {integrity: sha512-8b4LKFpcbJCzLslwcriPiFbr2fkmEBvzJzvF/Oc5xqzdT/cM6KEyX/w2brMqtA8b4lmEiODcXk4/4j7W/f9g7A==} + '@pgsql/quotes@18.1.0': resolution: {integrity: sha512-wZcMP1QHiQbWWKOw4nfvZ74xUSz/9jqeSUXVnoR1saI5M0D+iYbiuOlfNDyYmziLwgEkCuSJLZK1dZ+eQCwgAA==} + '@pgsql/quotes@18.2.4': + resolution: {integrity: sha512-muCDdPu/bM/f9d7IquBNfkMgiRPehv6vvjdhSMxXeVKwJKPX9xIscDu11ju15s7UeEXoWntE8zrSaR9wQ9wgCg==} + + '@pgsql/scripts@18.4.5': + resolution: {integrity: sha512-hXv+csLboEX11UtJ9qlnYEhg5HegmR3FD7WNrBy4cWIqmmOv2jvfdiG+sHjh9oOXm/BSIiSPhvkgWWUJl94CLg==} + + '@pgsql/semantics@18.1.6': + resolution: {integrity: sha512-ZdB+vBR+THE8JfwpGJ/A/Fw7s4fARXtk1yiIzeoK2BXtEiqem9UBR1GGL9+mKLu4+zvxYaeelhQQZKFXNu1KQA==} + + '@pgsql/transform@18.17.5': + resolution: {integrity: sha512-BPENEY7aZYparGSqKYmAd28heQ3oBX72mAdAcwS8iJciz0K082tqGVTopgS9EAN+mY6YTbIHxkhxK7BhPuOp7Q==} + '@pgsql/transform@18.3.1': resolution: {integrity: sha512-eWYZ4OQucZo4ZGtf6KYw/uE8hrAwn4PH4Gp+cUfVzsx3CKBChJ/dYbTAsFjMFfG+5IbAQQNGvkLbWFJ0a1gTWQ==} '@pgsql/traverse@18.3.0': resolution: {integrity: sha512-5DOD9OExBT8wvejdQavnpWlI9625w2j3Mi+ecvCUxaXz/R+Lesj3lsp8Bp7gmUFEOJ7hbhDr0zCzfJkjPHjsJw==} + '@pgsql/traverse@18.7.7': + resolution: {integrity: sha512-WHPNa+2/g5lpAAKHxAizSci84d8FXnBK76pqHIKc9UeyRcAmq3gjnSww6z9AZcUXRVuzf6xpPpN68z1jAMdk6w==} + '@pgsql/types@18.0.0': resolution: {integrity: sha512-jtAY4JU7jdVYZ/Vv3zlZqVP+FRWZokyCLKPPCnAApiaJAElpyyiMC3HRiquBu29kSrVs+rkqZD50SUIb/zyEGw==} '@pgsql/utils@18.1.1': resolution: {integrity: sha512-ltmbZOQQXKyPs94DDbDl+SQT9Wvf3B6oyT6vDf0dPbv/LOiGm5vD5SiUOofVPh9cTlX/QRVpOVJkhRsknIK8lg==} + '@pgsql/utils@18.2.8': + resolution: {integrity: sha512-BgWSjJ8McbAppf1kw3n5SGD/B8yEh3wk3d3l5IMGz2YBfvHfuXx0qpfpT0g6PVgONwYVk33jpaKvyoNhHDPMTg==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -2148,6 +2208,9 @@ packages: appstash@0.7.0: resolution: {integrity: sha512-UExc8kEseReJRbllAkQ/qeW+jHb4iVFR8bLfggSLvSO7LwiVjQWfnQxN+ToLkVBKqMbIENrLUTvynMSEC73xUg==} + appstash@0.8.0: + resolution: {integrity: sha512-BGt/TxAwPO6c30df8FnGwy0w3UvH4MBYQza+nDTxbektw6eBFzyCGt7qzBWRiml2YNNW8ebzoKpK2XfXNQkyrw==} + aproba@2.0.0: resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} @@ -2264,6 +2327,10 @@ packages: resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} engines: {node: 18 || 20 || >=22} + brace-expansion@5.0.9: + resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} + engines: {node: 20 || >=22} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -2544,6 +2611,10 @@ packages: engines: {node: '>= 10'} hasBin: true + csv-to-pg@4.10.4: + resolution: {integrity: sha512-Z12oBtyxzYTWod049pg3qhbC0vS+d/9uUeDvkzcAQfcq1sCH8s4H3EBYZC754wNlDTnTDKgOZJCQzxOBGzwuFA==} + hasBin: true + csv-to-pg@4.4.0: resolution: {integrity: sha512-GhEGmpuGrnMgPXE519hAOexiibx/Gnr5dGIRsQ/bE0ml0LAwnbB4gjPfInPcB6+4k14ERsIJ53pRqKgOsWfChg==} hasBin: true @@ -3013,6 +3084,9 @@ packages: genomic@5.6.2: resolution: {integrity: sha512-y2LK1KQjeZZ4WT0DEQhjTxMNs+hsoZTclIqdnU5Xo3Ie8phDB6ynw8Sk2NLtELL7H3Q26tvZBJkckIkaSa0Lag==} + genomic@5.6.5: + resolution: {integrity: sha512-gmCNjYH+nS/moerbcXZiREN9vbK8E4nETlQzLuIuaH0kG7WYV8nKR8ckb1vw/WBXvo1Z8XIBCFvxnPsssiRA6Q==} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -3058,6 +3132,10 @@ packages: resolution: {integrity: sha512-mKZj9JLQrwMBtj5wxi6MH8Z5eSKaERpAwjg43dPtlGI1ZVEgH/qC7T8/6R2OBSUA+zzHBZgICsVJaEIV2tKTDA==} engines: {node: '>=6.0'} + git-changed@0.4.0: + resolution: {integrity: sha512-5jTYI8c+PY9uO5H7aEAJq5uPFp7l77qe5so7rimlCVhbg1R5SKkD1j5BPSu56MvWxcsLGEUo6nrO2uR8ZyOEew==} + hasBin: true + git-raw-commits@3.0.0: resolution: {integrity: sha512-b5OHmZ3vAgGrDn/X0kS+9qCfNKWe4K/jFnhwzVWWg0/k5eLa3060tZShrRg8Dja5kPc+YjS0Gc6y7cRr44Lpjw==} engines: {node: '>=14'} @@ -3548,6 +3626,9 @@ packages: inquirerer@4.9.1: resolution: {integrity: sha512-RXgbivwNs9luseSHnjIcJ05A8VRGcdCQuLjyWWBmI1zwyorkSzCw4KyK8634a5KrbbXehSJHBE3rIrtgJJiBEQ==} + inquirerer@4.9.3: + resolution: {integrity: sha512-f3iJubKDBE5Cp9NnJ0cRGt9zngbZ78b62QvrbRjwS13VX+s+fcJvcMaEv/3Es46dIyjDtFfhvGZa9/krNJnSbw==} + interpret@3.1.1: resolution: {integrity: sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==} engines: {node: '>=10.13.0'} @@ -3946,6 +4027,9 @@ packages: libpg-query@18.1.2: resolution: {integrity: sha512-8epkLd4hMoUVfYu2RlXWiUTxnGw1S6adm6/rMgWqlUB6+KsVb5BmJrDY8CjaPB9NWbEt6OnvQnfGxol2iBxn4A==} + libpg-query@18.1.4: + resolution: {integrity: sha512-6jF9qn9ln4yLnmSF+bS/DBvAfNcPhGI05+pkQy+n3cRd3p9aO8LWjquEin8SLO0k7zYk745ZGBXYUiL9AUI7gA==} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -4133,6 +4217,10 @@ packages: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} + engines: {node: 18 || 20 || >=22} + minimatch@3.0.5: resolution: {integrity: sha512-tUpxzX0VAzJHjLu0xUfFv1gwVp9ba3IOuRAVH2EGuRW8a5emA2FlACLqiT/lDVtS1W+TGNwqz3sWaNyLgDJWuw==} @@ -4548,6 +4636,9 @@ packages: pg-cache@3.19.0: resolution: {integrity: sha512-dFM0p6iCS2zTJWiiAdcNsJUCU07yJopd1tG0shxUjEG6URJJ0y0NTsgc7f8yfmVP09QJXGLi4dvnP7DRXWWh1Q==} + pg-cache@3.25.4: + resolution: {integrity: sha512-uf8Dcot//6oMQkdXYE5To7PbTGNTK1b/pgVlsUFKr3ODsJjxa4WeYMPFegW7mvAGZVsaeSnLwnpk1kx3bt69Bg==} + pg-cloudflare@1.4.0: resolution: {integrity: sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==} @@ -4560,6 +4651,9 @@ packages: pg-env@1.22.0: resolution: {integrity: sha512-IH4VI5cAs6UGCqkw2sjaBbghoK5L++lPUsVkRxzCh+NJFR947VV8BmkDS5De1QIeX4NFaEadwvo9U+Bouz5hpg==} + pg-env@1.29.1: + resolution: {integrity: sha512-0XkYQgZf/+dNrNYIYrEVQsbtd92hP9FYPbaJ8NLzRWQ/GOqtGTk9KW5M3OS4HI1ii4AGzClyX5CW3IAZj0/aYQ==} + pg-int8@1.0.1: resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} engines: {node: '>=4.0.0'} @@ -4576,14 +4670,17 @@ packages: pg-proto-parser@1.31.0: resolution: {integrity: sha512-9ET3EXX4Xa89t1WkpTW6VC4LkRoFnC+On4nukPMvllqSEmM9so3sG1yRdHm6jD24lw7qEQgMEQy7/4hytXyZfw==} + pg-proto-parser@1.32.5: + resolution: {integrity: sha512-rYfjXTwzZDpTpA6h2Yl9oYu1GPHgmZWOkFxjrg3cMcRASu+F+FaYp1+8JvOBl05rA+OCOlqsq3sViaR5xOlMIQ==} + pg-protocol@1.15.0: resolution: {integrity: sha512-cq9sECI5s0+uPUXjbz8ioyPJni6RzsRib0US67i5IoTZKw8fNeYlVE7u8F4dG7vEJJtc5wdD1K189lCCUwqWTQ==} pg-query-context@2.23.0: resolution: {integrity: sha512-ZlGHx1S1BXIqSq+s4vVmMOWoU4YKTlFF/cgcDEhFbUe1udOtB0Q5z55eIifA0FRKyADFttYvHxH/KePLKaCRzQ==} - pg-seed@0.21.0: - resolution: {integrity: sha512-u1BL3R6v/jdtSnQNl2d8q685xDriyWpp6uk3D6bnNDJCAO5S1IspwMmMv4ZclmTim7QPbi/kdaXWxX8EKrVnYg==} + pg-seed@0.27.1: + resolution: {integrity: sha512-4kpvlS0Zn+FFTNliCUvcThtmk2VX+fp0crR8XAOA3uLXEknD6J9u31PFSpQ14XGhBXuXughLkZZqzPGhlt3/2A==} pg-sql2@5.0.1: resolution: {integrity: sha512-DdOZNUBhuBuGcq3UgUNsYE9FPdPzw9YA1K/WQxutNhC17DA/CImapyamv6lliVvdAVNZ+CWB1z+p7SmSJmkezw==} @@ -4609,20 +4706,26 @@ packages: resolution: {integrity: sha512-ucIIEYKza605UEFIY+6Hy02W5KThwEZC1Ed3Ym4AH+PWtquz32JZgcH650vF9YLBc/7PwBARvN9/90+ww1hCHg==} hasBin: true - pgsql-client@4.4.1: - resolution: {integrity: sha512-RfDK+Tbti6J+cBfknkPQSJVqp+0QU92Vj4SOqgo15gI3ye91ZMHfbpF2IDo/MTtPRTQiXEu4nzBdYXkIAM8G4Q==} + pgsql-client@4.10.7: + resolution: {integrity: sha512-M4m7EwIb5tBHQOLfbi7ZvoLcu6QkwImiRjHlasfsPaMxn0J0uEvPsT6Mii/EhwTiF+xdP/AR+ZznAVN4YUGY+Q==} pgsql-deparser@18.1.1: resolution: {integrity: sha512-G+5yj8rGlkW5YUJTKubV3J5YnAvbm8YpzphT1bYkRjAI5MKTVRXwGZJ0gqluqjhLNqDw6d20IFE4J5Z7yEmwWg==} + pgsql-deparser@18.3.6: + resolution: {integrity: sha512-JJOkNr3JqKNEqmf0mBb7/2HAU9kh75AX81oB6ppstFuLVeVBYoWfSgnZVSmMWRgwtjEnYL2+IUpk/Ir7fvceTg==} + pgsql-parser@18.1.1: resolution: {integrity: sha512-wMOCDIuj7TOGy1gxYn2B0/y2row8sOIbPJ4IMkLD0fpgwgGZN26TYK+05AG27WExgzp9JGr9EbTaCF8T1sVzrQ==} - pgsql-seed@3.4.1: - resolution: {integrity: sha512-jkSHCWr0cwtQaZQKKQWZL3e06KS2vm86poGFKd0tb3ovCFa94R3Ldi0IAjRh/L4oAUuXH/GXf+rzibTCJQyUQg==} + pgsql-parser@18.2.6: + resolution: {integrity: sha512-PC/d/Ht0cpBSnA4aYd/UOf9YV1pgKFnX70xfIRC6r30SvzTLgnLs0iR5BlMZrQrBeiKnL3pc7yDvIJjTi0isfw==} + + pgsql-seed@3.10.7: + resolution: {integrity: sha512-9l73YCDjEQ97NsTk7Xwv2g/Nbf0zv3QRXa3h6bYmhXgUAA0+5JlyNP2zdC7UVtbJklR5Vm2phfLRbqsZSQMmLQ==} - pgsql-test@5.4.1: - resolution: {integrity: sha512-7Z0sSxLYhUPa4NFZaKeCgIDRPBhbEV859NKNnGzH9nEFvJPRWY8iK4bISgrT8wwLi8j+hWH+f6zFU7WAqHqrTw==} + pgsql-test@5.10.7: + resolution: {integrity: sha512-kTYd3eHj758t8w9aExuyUAhXYxnA+vkxFh0iKqZucH7npMckEVq7XxkvA7x+UHmnyF/HQwyTFs+Z8k31P6fKHQ==} picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -4666,9 +4769,15 @@ packages: plpgsql-deparser@18.1.2: resolution: {integrity: sha512-r9uqdSAU+9hve08jFpCoYHe1C7IbuIANhOAJFMNZVcPzoxAhvTRNlngLnzlrVQUXADj7Jcqcn+848SWdOLBktA==} + plpgsql-deparser@18.2.7: + resolution: {integrity: sha512-n56eIFH0DREyrIXbG2hikiXzs2NCuM2pv5nkbUIVhy8nOnMQ1BqdvUUdmoSWcNzSvasCgPA7pUXBaB6tqjwXHw==} + plpgsql-parser@18.2.1: resolution: {integrity: sha512-ydrn6KcCqVDN9XM4PNz7qAiFl5Nk/tf/hXa2l3KDZAuCPAu1lYp3iIqyFzr0jCebFJ4nTxR96eQpFUsZUgzLJw==} + plpgsql-parser@18.5.7: + resolution: {integrity: sha512-Mp5sttG2nC0CIAypLzytLIngpUbu2g3m+rqccgbYUmFk7RCBnvfCZWLp4NKFBIPWzoszmZ+VqjxBJ5L07c3vUA==} + pluralize@7.0.0: resolution: {integrity: sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==} engines: {node: '>=4'} @@ -5627,6 +5736,9 @@ packages: yanse@0.2.1: resolution: {integrity: sha512-SMi3ZO1IqsvPLLXuy8LBCP1orqcjOT8VygiuyAlplaGeH2g+n4ZSSyWlA/BZjuUuN58TyOcz89mVkflSqIPxxQ==} + yanse@0.2.2: + resolution: {integrity: sha512-T+4OAK9/idBEgH0M6WwFT5k55OOcNLBbheVgJ5qqBchzUA+DN7SfHBDi47zsQeXcBg6Mf5z9FFASTxPwAGbO2A==} + yargs-parser@20.2.9: resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} engines: {node: '>=10'} @@ -5675,6 +5787,10 @@ snapshots: dependencies: envalid: 8.2.0 + 12factor-env@1.27.1: + dependencies: + envalid: 8.2.0 + '@0no-co/graphql.web@1.2.0(graphql@16.13.0)': optionalDependencies: graphql: 16.13.0 @@ -6133,6 +6249,8 @@ snapshots: mime-bytes: 0.24.0 uuid-hash: 2.23.0 + '@constructive-io/errors@0.10.0': {} + '@constructive-io/express-context@0.18.0(express@5.2.1)': dependencies: '@constructive-io/url-domains': 2.23.0 @@ -7078,16 +7196,52 @@ snapshots: dependencies: '@octokit/openapi-types': 24.2.0 + '@pgpmjs/ast@0.11.3': + dependencies: + '@pgpmjs/env': 2.41.3 + '@pgpmjs/types': 2.50.3 + '@pgpmjs/ast@0.5.0': dependencies: '@pgpmjs/env': 2.33.0 '@pgpmjs/types': 2.40.0 + '@pgpmjs/bundle@0.14.3': + dependencies: + '@pgpmjs/ast': 0.11.3 + '@pgpmjs/traverse': 0.11.3 + '@pgpmjs/bundle@0.6.0': dependencies: '@pgpmjs/ast': 0.5.0 '@pgpmjs/traverse': 0.5.0 + '@pgpmjs/core@7.25.4': + dependencies: + '@pgpmjs/ast': 0.11.3 + '@pgpmjs/bundle': 0.14.3 + '@pgpmjs/diff': 0.3.5 + '@pgpmjs/env': 2.41.3 + '@pgpmjs/logger': 2.24.1 + '@pgpmjs/server-utils': 3.25.4 + '@pgpmjs/slice': 0.14.6 + '@pgpmjs/transform': 0.23.4 + '@pgpmjs/types': 2.50.3 + csv-to-pg: 4.10.4 + genomic: 5.6.5 + git-changed: 0.4.0 + glob: 13.0.6 + parse-package-name: 1.0.0 + pg: 8.22.0 + pg-cache: 3.25.4 + pg-env: 1.29.1 + pgsql-deparser: 18.3.6 + pgsql-parser: 18.2.6 + yanse: 0.2.2 + transitivePeerDependencies: + - pg-native + - supports-color + '@pgpmjs/core@7.9.0': dependencies: '@pgpmjs/ast': 0.5.0 @@ -7111,12 +7265,26 @@ snapshots: - pg-native - supports-color + '@pgpmjs/diff@0.3.5': + dependencies: + '@pgpmjs/ast': 0.11.3 + '@pgpmjs/naming-spec': 0.3.0 + '@pgpmjs/transform': 0.23.4 + transitivePeerDependencies: + - supports-color + '@pgpmjs/env@2.33.0': dependencies: 12factor-env: 1.21.0 '@pgpmjs/types': 2.40.0 deepmerge: 4.3.1 + '@pgpmjs/env@2.41.3': + dependencies: + 12factor-env: 1.27.1 + '@pgpmjs/types': 2.50.3 + deepmerge: 4.3.1 + '@pgpmjs/export@1.8.1(@dataplan/json@1.0.0(grafast@1.0.2(graphql@16.13.0)))(@dataplan/pg@1.0.3(@dataplan/json@1.0.0(grafast@1.0.2(graphql@16.13.0)))(grafast@1.0.2(graphql@16.13.0))(graphile-config@1.0.1)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.22.0))(@types/node@22.19.17)(grafserv@1.0.0(@types/node@22.19.17)(grafast@1.0.2(graphql@16.13.0))(graphile-config@1.0.1)(graphql@16.13.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5))(ws@8.20.0))(graphile-build@5.0.2(grafast@1.0.2(graphql@16.13.0))(graphile-config@1.0.1)(graphql@16.13.0))(pg-sql2@5.0.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tamedevil@0.1.1)(use-sync-external-store@1.6.0(react@19.2.5))(ws@8.20.0)': dependencies: '@constructive-io/query-builder': 3.4.0 @@ -7160,6 +7328,10 @@ snapshots: dependencies: yanse: 0.2.1 + '@pgpmjs/logger@2.24.1': + dependencies: + yanse: 0.2.2 + '@pgpmjs/migrate-client@1.5.1(@dataplan/json@1.0.0(grafast@1.0.2(graphql@16.13.0)))(@dataplan/pg@1.0.3(@dataplan/json@1.0.0(grafast@1.0.2(graphql@16.13.0)))(grafast@1.0.2(graphql@16.13.0))(graphile-config@1.0.1)(graphql@16.13.0)(pg-sql2@5.0.1)(pg@8.22.0))(@types/node@22.19.17)(grafserv@1.0.0(@types/node@22.19.17)(grafast@1.0.2(graphql@16.13.0))(graphile-config@1.0.1)(graphql@16.13.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5))(ws@8.20.0))(graphile-build@5.0.2(grafast@1.0.2(graphql@16.13.0))(graphile-config@1.0.1)(graphql@16.13.0))(pg-sql2@5.0.1)(pg@8.22.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(tamedevil@0.1.1)(use-sync-external-store@1.6.0(react@19.2.5))(ws@8.20.0)': dependencies: '@0no-co/graphql.web': 1.2.0(graphql@16.13.0) @@ -7194,6 +7366,8 @@ snapshots: - utf-8-validate - ws + '@pgpmjs/naming-spec@0.3.0': {} + '@pgpmjs/server-utils@3.19.0': dependencies: '@pgpmjs/logger': 2.18.0 @@ -7204,6 +7378,25 @@ snapshots: transitivePeerDependencies: - supports-color + '@pgpmjs/server-utils@3.25.4': + dependencies: + '@pgpmjs/logger': 2.24.1 + '@pgpmjs/types': 2.50.3 + cors: 2.8.6 + express: 5.2.1 + lru-cache: 11.3.5 + transitivePeerDependencies: + - supports-color + + '@pgpmjs/slice@0.14.6': + dependencies: + '@pgpmjs/ast': 0.11.3 + '@pgpmjs/transform': 0.23.4 + minimatch: 10.2.6 + plpgsql-parser: 18.5.7 + transitivePeerDependencies: + - supports-color + '@pgpmjs/slice@0.5.0': dependencies: '@pgpmjs/ast': 0.5.0 @@ -7213,6 +7406,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@pgpmjs/transform@0.23.4': + dependencies: + '@pgpmjs/ast': 0.11.3 + '@pgpmjs/naming-spec': 0.3.0 + '@pgsql/scripts': 18.4.5 + '@pgsql/semantics': 18.1.6 + '@pgsql/transform': 18.17.5 + plpgsql-parser: 18.5.7 + transitivePeerDependencies: + - supports-color + '@pgpmjs/transform@0.5.0': dependencies: '@pgsql/transform': 18.3.1 @@ -7220,6 +7424,10 @@ snapshots: transitivePeerDependencies: - supports-color + '@pgpmjs/traverse@0.11.3': + dependencies: + '@pgpmjs/ast': 0.11.3 + '@pgpmjs/traverse@0.5.0': dependencies: '@pgpmjs/ast': 0.5.0 @@ -7228,8 +7436,39 @@ snapshots: dependencies: pg-env: 1.22.0 + '@pgpmjs/types@2.50.3': + dependencies: + '@constructive-io/errors': 0.10.0 + pg-env: 1.29.1 + '@pgsql/quotes@18.1.0': {} + '@pgsql/quotes@18.2.4': {} + + '@pgsql/scripts@18.4.5': + dependencies: + '@pgsql/quotes': 18.2.4 + '@pgsql/transform': 18.17.5 + plpgsql-parser: 18.5.7 + transitivePeerDependencies: + - supports-color + + '@pgsql/semantics@18.1.6': + dependencies: + '@pgsql/traverse': 18.7.7 + plpgsql-parser: 18.5.7 + transitivePeerDependencies: + - supports-color + + '@pgsql/transform@18.17.5': + dependencies: + '@pgsql/quotes': 18.2.4 + '@pgsql/semantics': 18.1.6 + '@pgsql/traverse': 18.7.7 + plpgsql-parser: 18.5.7 + transitivePeerDependencies: + - supports-color + '@pgsql/transform@18.3.1': dependencies: '@pgsql/quotes': 18.1.0 @@ -7245,6 +7484,14 @@ snapshots: transitivePeerDependencies: - supports-color + '@pgsql/traverse@18.7.7': + dependencies: + '@pgsql/types': 18.0.0 + pg-proto-parser: 1.32.5 + plpgsql-deparser: 18.2.7 + transitivePeerDependencies: + - supports-color + '@pgsql/types@18.0.0': {} '@pgsql/utils@18.1.1': @@ -7252,6 +7499,11 @@ snapshots: '@pgsql/types': 18.0.0 nested-obj: 0.2.2 + '@pgsql/utils@18.2.8': + dependencies: + '@pgsql/types': 18.0.0 + nested-obj: 0.2.3 + '@pkgjs/parseargs@0.11.0': optional: true @@ -7978,6 +8230,8 @@ snapshots: appstash@0.7.0: {} + appstash@0.8.0: {} + aproba@2.0.0: {} arg@4.1.3: {} @@ -8127,6 +8381,10 @@ snapshots: dependencies: balanced-match: 4.0.4 + brace-expansion@5.0.9: + dependencies: + balanced-match: 4.0.4 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -8307,7 +8565,7 @@ snapshots: constructive-test@1.4.1: dependencies: - pgsql-test: 5.4.1 + pgsql-test: 5.10.7 transitivePeerDependencies: - pg-native - supports-color @@ -8404,6 +8662,15 @@ snapshots: csv-parser@3.2.1: {} + csv-to-pg@4.10.4: + dependencies: + '@pgsql/types': 18.0.0 + '@pgsql/utils': 18.2.8 + csv-parser: 3.2.1 + inquirerer: 4.9.3 + js-yaml: 4.1.1 + pgsql-deparser: 18.3.6 + csv-to-pg@4.4.0: dependencies: '@pgsql/types': 18.0.0 @@ -8859,6 +9126,11 @@ snapshots: appstash: 0.7.0 inquirerer: 4.9.1 + genomic@5.6.5: + dependencies: + appstash: 0.8.0 + inquirerer: 4.9.3 + gensync@1.0.0-beta.2: {} get-caller-file@2.0.5: {} @@ -8902,6 +9174,8 @@ snapshots: dependencies: isobject: 3.0.1 + git-changed@0.4.0: {} + git-raw-commits@3.0.0: dependencies: dargs: 7.0.0 @@ -9574,6 +9848,13 @@ snapshots: minimist: 1.2.8 yanse: 0.2.1 + inquirerer@4.9.3: + dependencies: + deepmerge: 4.3.1 + find-and-require-package-json: 0.9.1 + minimist: 1.2.8 + yanse: 0.2.2 + interpret@3.1.1: {} ip-address@10.1.0: {} @@ -10221,6 +10502,10 @@ snapshots: dependencies: '@pgsql/types': 18.0.0 + libpg-query@18.1.4: + dependencies: + '@pgsql/types': 18.0.0 + lines-and-columns@1.2.4: {} lines-and-columns@2.0.3: {} @@ -10401,6 +10686,10 @@ snapshots: dependencies: brace-expansion: 5.0.5 + minimatch@10.2.6: + dependencies: + brace-expansion: 5.0.9 + minimatch@3.0.5: dependencies: brace-expansion: 1.1.12 @@ -10890,6 +11179,17 @@ snapshots: transitivePeerDependencies: - pg-native + pg-cache@3.25.4: + dependencies: + 12factor-env: 1.27.1 + '@pgpmjs/logger': 2.24.1 + '@pgpmjs/types': 2.50.3 + lru-cache: 11.3.5 + pg: 8.22.0 + pg-env: 1.29.1 + transitivePeerDependencies: + - pg-native + pg-cloudflare@1.4.0: optional: true @@ -10901,6 +11201,10 @@ snapshots: dependencies: 12factor-env: 1.21.0 + pg-env@1.29.1: + dependencies: + 12factor-env: 1.27.1 + pg-int8@1.0.1: {} pg-introspection@1.0.1: @@ -10925,6 +11229,20 @@ snapshots: transitivePeerDependencies: - supports-color + pg-proto-parser@1.32.5: + dependencies: + '@babel/generator': 7.28.5 + '@babel/parser': 7.28.5 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 + '@launchql/protobufjs': 7.2.6 + case: 1.6.3 + deepmerge: 4.3.1 + nested-obj: 0.2.3 + strfy-js: 3.2.2 + transitivePeerDependencies: + - supports-color + pg-protocol@1.15.0: {} pg-query-context@2.23.0: @@ -10933,7 +11251,7 @@ snapshots: transitivePeerDependencies: - pg-native - pg-seed@0.21.0: + pg-seed@0.27.1: dependencies: csv-parse: 6.2.1 pg: 8.22.0 @@ -11015,13 +11333,13 @@ snapshots: - utf-8-validate - ws - pgsql-client@4.4.1: + pgsql-client@4.10.7: dependencies: - '@pgpmjs/core': 7.9.0 - '@pgpmjs/logger': 2.18.0 - '@pgpmjs/types': 2.40.0 + '@pgpmjs/core': 7.25.4 + '@pgpmjs/logger': 2.24.1 + '@pgpmjs/types': 2.50.3 pg: 8.22.0 - pg-env: 1.22.0 + pg-env: 1.29.1 transitivePeerDependencies: - pg-native - supports-color @@ -11031,34 +11349,45 @@ snapshots: '@pgsql/quotes': 18.1.0 '@pgsql/types': 18.0.0 + pgsql-deparser@18.3.6: + dependencies: + '@pgsql/quotes': 18.2.4 + '@pgsql/types': 18.0.0 + pgsql-parser@18.1.1: dependencies: '@pgsql/types': 18.0.0 libpg-query: 18.1.2 pgsql-deparser: 18.1.1 - pgsql-seed@3.4.1: + pgsql-parser@18.2.6: dependencies: - '@pgpmjs/core': 7.9.0 - '@pgpmjs/env': 2.33.0 + '@pgsql/types': 18.0.0 + libpg-query: 18.1.4 + pgsql-deparser: 18.3.6 + + pgsql-seed@3.10.7: + dependencies: + '@pgpmjs/core': 7.25.4 + '@pgpmjs/env': 2.41.3 pg: 8.22.0 - pg-env: 1.22.0 - pg-seed: 0.21.0 + pg-env: 1.29.1 + pg-seed: 0.27.1 transitivePeerDependencies: - pg-native - supports-color - pgsql-test@5.4.1: + pgsql-test@5.10.7: dependencies: - '@pgpmjs/env': 2.33.0 - '@pgpmjs/logger': 2.18.0 - '@pgpmjs/server-utils': 3.19.0 - '@pgpmjs/types': 2.40.0 + '@pgpmjs/env': 2.41.3 + '@pgpmjs/logger': 2.24.1 + '@pgpmjs/server-utils': 3.25.4 + '@pgpmjs/types': 2.50.3 pg: 8.22.0 - pg-cache: 3.19.0 - pg-env: 1.22.0 - pgsql-client: 4.4.1 - pgsql-seed: 3.4.1 + pg-cache: 3.25.4 + pg-env: 1.29.1 + pgsql-client: 4.10.7 + pgsql-seed: 3.10.7 transitivePeerDependencies: - pg-native - supports-color @@ -11090,6 +11419,11 @@ snapshots: '@pgsql/types': 18.0.0 pgsql-deparser: 18.1.1 + plpgsql-deparser@18.2.7: + dependencies: + '@pgsql/types': 18.0.0 + pgsql-deparser: 18.3.6 + plpgsql-parser@18.2.1: dependencies: '@pgsql/traverse': 18.3.0 @@ -11100,6 +11434,16 @@ snapshots: transitivePeerDependencies: - supports-color + plpgsql-parser@18.5.7: + dependencies: + '@pgsql/traverse': 18.7.7 + '@pgsql/types': 18.0.0 + libpg-query: 18.1.4 + pgsql-deparser: 18.3.6 + plpgsql-deparser: 18.2.7 + transitivePeerDependencies: + - supports-color + pluralize@7.0.0: {} pnpm-policy@0.2.2: @@ -12030,6 +12374,8 @@ snapshots: yanse@0.2.1: {} + yanse@0.2.2: {} + yargs-parser@20.2.9: {} yargs-parser@21.1.1: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index dbe7b6800..c27554d8d 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,7 +1,7 @@ packages: - packages/* -# Exempt from the wait: 6 scope glob(s), 52 first-party package(s). +# Exempt from the wait: 6 scope glob(s), 53 first-party package(s). # First-party membership comes from what pyramation publishes on npm — waiting on your own release protects nothing. minimumReleaseAgeExclude: - "@constructive-db/*" @@ -20,6 +20,7 @@ minimumReleaseAgeExclude: - etag-hash - find-and-require-package-json - genomic + - git-changed - gql-ast - graphile-bucket-provisioner-plugin - graphile-bulk-mutations