@memberjunction/codegen-lib 5.32.0 → 5.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +24 -0
  2. package/dist/Config/config.d.ts +19 -8
  3. package/dist/Config/config.d.ts.map +1 -1
  4. package/dist/Config/config.js +18 -8
  5. package/dist/Config/config.js.map +1 -1
  6. package/dist/Config/db-connection.d.ts.map +1 -1
  7. package/dist/Config/db-connection.js +8 -3
  8. package/dist/Config/db-connection.js.map +1 -1
  9. package/dist/Database/codeGenDatabaseProvider.d.ts +67 -0
  10. package/dist/Database/codeGenDatabaseProvider.d.ts.map +1 -1
  11. package/dist/Database/codeGenDatabaseProvider.js +82 -15
  12. package/dist/Database/codeGenDatabaseProvider.js.map +1 -1
  13. package/dist/Database/manage-metadata.d.ts +112 -4
  14. package/dist/Database/manage-metadata.d.ts.map +1 -1
  15. package/dist/Database/manage-metadata.js +347 -43
  16. package/dist/Database/manage-metadata.js.map +1 -1
  17. package/dist/Database/providers/postgresql/PostgreSQLCodeGenConnection.d.ts +4 -0
  18. package/dist/Database/providers/postgresql/PostgreSQLCodeGenConnection.d.ts.map +1 -1
  19. package/dist/Database/providers/postgresql/PostgreSQLCodeGenConnection.js +6 -0
  20. package/dist/Database/providers/postgresql/PostgreSQLCodeGenConnection.js.map +1 -1
  21. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts +122 -16
  22. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts.map +1 -1
  23. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js +605 -49
  24. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js.map +1 -1
  25. package/dist/Database/providers/postgresql/viewFallback.d.ts +14 -0
  26. package/dist/Database/providers/postgresql/viewFallback.d.ts.map +1 -1
  27. package/dist/Database/providers/postgresql/viewFallback.js +107 -13
  28. package/dist/Database/providers/postgresql/viewFallback.js.map +1 -1
  29. package/dist/Database/providers/sqlserver/SQLServerCodeGenConnection.d.ts +4 -0
  30. package/dist/Database/providers/sqlserver/SQLServerCodeGenConnection.d.ts.map +1 -1
  31. package/dist/Database/providers/sqlserver/SQLServerCodeGenConnection.js +6 -0
  32. package/dist/Database/providers/sqlserver/SQLServerCodeGenConnection.js.map +1 -1
  33. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts +5 -2
  34. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts.map +1 -1
  35. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js +27 -7
  36. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js.map +1 -1
  37. package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.test.js +101 -0
  38. package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.test.js.map +1 -1
  39. package/dist/Database/sql.d.ts +8 -0
  40. package/dist/Database/sql.d.ts.map +1 -1
  41. package/dist/Database/sql.js +18 -14
  42. package/dist/Database/sql.js.map +1 -1
  43. package/dist/Database/sql_codegen.d.ts +19 -5
  44. package/dist/Database/sql_codegen.d.ts.map +1 -1
  45. package/dist/Database/sql_codegen.js +109 -55
  46. package/dist/Database/sql_codegen.js.map +1 -1
  47. package/dist/Misc/action_subclasses_codegen.js +1 -1
  48. package/dist/Misc/action_subclasses_codegen.js.map +1 -1
  49. package/dist/Misc/createNewUser.d.ts.map +1 -1
  50. package/dist/Misc/createNewUser.js +14 -3
  51. package/dist/Misc/createNewUser.js.map +1 -1
  52. package/dist/Misc/entity_subclasses_codegen.d.ts.map +1 -1
  53. package/dist/Misc/entity_subclasses_codegen.js +23 -17
  54. package/dist/Misc/entity_subclasses_codegen.js.map +1 -1
  55. package/dist/Misc/graphql_server_codegen.d.ts +11 -0
  56. package/dist/Misc/graphql_server_codegen.d.ts.map +1 -1
  57. package/dist/Misc/graphql_server_codegen.js +27 -40
  58. package/dist/Misc/graphql_server_codegen.js.map +1 -1
  59. package/dist/Misc/sql_logging.d.ts.map +1 -1
  60. package/dist/Misc/sql_logging.js +22 -2
  61. package/dist/Misc/sql_logging.js.map +1 -1
  62. package/dist/Misc/system_integrity.js +3 -3
  63. package/dist/Misc/system_integrity.js.map +1 -1
  64. package/dist/Misc/temp_batch_file.js +2 -2
  65. package/dist/Misc/temp_batch_file.js.map +1 -1
  66. package/dist/runCodeGen.d.ts +1 -1
  67. package/dist/runCodeGen.d.ts.map +1 -1
  68. package/dist/runCodeGen.js +51 -6
  69. package/dist/runCodeGen.js.map +1 -1
  70. package/package.json +19 -18
@@ -6,11 +6,13 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
6
6
  };
7
7
  var PostgreSQLCodeGenProvider_1;
8
8
  import { EntityInfo } from '@memberjunction/core';
9
- import { RegisterClass } from '@memberjunction/global';
9
+ import { RegisterClass, UUIDsEqual } from '@memberjunction/global';
10
10
  import { CodeGenDatabaseProvider, CRUDType, } from '../../codeGenDatabaseProvider.js';
11
11
  import { configInfo } from '../../../Config/config.js';
12
12
  import { logError } from '../../../Misc/status_logging.js';
13
13
  import { PostgreSQLDialect } from '@memberjunction/sql-dialect';
14
+ import { shouldIncludeFieldInParams, useJsonArgShape, } from '@memberjunction/generic-database-provider';
15
+ import { POSTGRESQL_PROCEDURE_PARAM_LIMIT } from '@memberjunction/postgresql-dataprovider';
14
16
  import * as fs from 'fs';
15
17
  import path from 'path';
16
18
  import { executeWithFallback } from './viewFallback.js';
@@ -19,6 +21,10 @@ const pgDialect = new PostgreSQLDialect();
19
21
  * PostgreSQL implementation of the CodeGen database provider.
20
22
  * Generates PostgreSQL-native DDL for views, CRUD functions, triggers, indexes,
21
23
  * full-text search, permissions, and other database objects.
24
+ *
25
+ * Registered with `MJGlobal.ClassFactory` against the canonical `'postgresql'`
26
+ * platform key — `SQLCodeGenBase` resolves this provider via
27
+ * `ClassFactory.CreateInstance(CodeGenDatabaseProvider, configInfo.dbPlatform)`.
22
28
  */
23
29
  let PostgreSQLCodeGenProvider = class PostgreSQLCodeGenProvider extends CodeGenDatabaseProvider {
24
30
  static { PostgreSQLCodeGenProvider_1 = this; }
@@ -30,6 +36,27 @@ let PostgreSQLCodeGenProvider = class PostgreSQLCodeGenProvider extends CodeGenD
30
36
  get PlatformKey() {
31
37
  return 'postgresql';
32
38
  }
39
+ /**
40
+ * @inheritdoc
41
+ *
42
+ * PostgreSQL: NOT supported. PG's strict `CREATE OR REPLACE VIEW` parser
43
+ * resolves view names against the existing catalog state at parse time, so
44
+ * a body that LEFT-JOINs the view-being-created to itself (e.g. for a
45
+ * self-FK's virtual NameField) raises `42P01 undefined_table` on first
46
+ * creation. A `CREATE OR REPLACE` retry against a NULL-typed stub then
47
+ * fails with `cannot change data type of view column ... from text to
48
+ * character varying(N)` because PG enforces strict column-type compat in
49
+ * `CREATE OR REPLACE`.
50
+ *
51
+ * Returning `false` tells `sql_codegen.ts` to skip the self-join entirely
52
+ * for self-FK + virtual-NameField cases. The trade-off: the corresponding
53
+ * virtual column (e.g. `RestoredFrom` on `vwRecordChanges`) is not emitted.
54
+ * Matches the baseline-shipped vwRecordChanges shape, which never had this
55
+ * column either.
56
+ */
57
+ canSelfJoinViewForVirtualNameField() {
58
+ return false;
59
+ }
33
60
  // ─── DROP GUARDS ─────────────────────────────────────────────────────
34
61
  /**
35
62
  * Generates a PostgreSQL `DROP ... IF EXISTS ... CASCADE` statement as a guard before
@@ -55,25 +82,43 @@ let PostgreSQLCodeGenProvider = class PostgreSQLCodeGenProvider extends CodeGenD
55
82
  }
56
83
  // ─── BASE VIEWS ──────────────────────────────────────────────────────
57
84
  /**
58
- * Generates a PostgreSQL `CREATE OR REPLACE VIEW` statement for an entity's base view.
85
+ * Generates a PostgreSQL view-regeneration block for an entity's base view.
86
+ *
59
87
  * Includes all base table columns, parent/related field joins, and root field lateral
60
88
  * joins. Applies a soft-delete `WHERE` filter when the entity uses soft deletes.
61
89
  *
62
- * **Non-destructive strategy.** Historically this method emitted
63
- * `DROP VIEW IF EXISTS ... CASCADE;` before the CREATE, which let it handle every
64
- * column-signature change — but also silently destroyed any dependent view, function,
65
- * trigger, or GRANT on the view. When a later statement (or subsequent entity in the
66
- * same run) failed, half the database's objects could be gone with no error surfaced.
90
+ * **Two-path emission (try-then-fallback).** The output wraps `CREATE OR REPLACE VIEW`
91
+ * in a `DO $$ ... EXCEPTION WHEN invalid_table_definition THEN DROP VIEW ... CASCADE;
92
+ * EXECUTE vsql; END $$` block. Why:
93
+ *
94
+ * - Happy path: `CREATE OR REPLACE VIEW` succeeds (new column list is a prefix of
95
+ * the existing one plus optional trailing additions). Zero destruction. No
96
+ * dependent views, functions, triggers, or grants are touched.
67
97
  *
68
- * We now emit just `CREATE OR REPLACE VIEW`, which PostgreSQL accepts when the new
69
- * column list is a prefix of the existing one plus optional trailing additions. Any
70
- * incompatible change (rename, reorder, type change, removed column) fails with
71
- * SQLSTATE `42P16 invalid_table_definition`. That error is intentionally allowed to
72
- * propagate up through `executeSQLFileViaShell` → the per-entity batch loop → and is
73
- * surfaced as a real regeneration failure. A follow-up pass will add a capture-drop-
74
- * restore fallback for 42P16 that preserves dependents via `pg_depend` and
75
- * `pg_get_viewdef` / `pg_get_functiondef`. Until that lands, 42P16 on PG is loud and
76
- * actionable instead of silent and destructive.
98
+ * - Sad path: PG raises SQLSTATE `42P16 invalid_table_definition` for any column
99
+ * rename / reorder / type change / removal. The exception handler runs
100
+ * `DROP VIEW ... CASCADE` and re-executes the CREATE. Dependent codegen-managed
101
+ * functions (spCreate/spUpdate/spDelete returning `SETOF vwFoo`) and dependent
102
+ * views are CASCADE-dropped — they are regenerated later in the same codegen
103
+ * output stream, so by the end of the run all dependents are restored to the new
104
+ * shape. GRANTs on the view itself are also lost on the CASCADE; codegen always
105
+ * re-emits permissions immediately after the view, so they come back too.
106
+ *
107
+ * The runtime-apply path also calls this, so the live DB applies the same DO block.
108
+ * `executeWithFallback` (the runtime helper) becomes a no-op for these statements
109
+ * because the DO block handles 42P16 internally — but it still runs as a safety net
110
+ * for any other failure modes.
111
+ *
112
+ * **What this DOES NOT preserve on the sad path:** non-codegen-managed dependent
113
+ * objects (e.g. a hand-written sproc against this view that codegen doesn't know
114
+ * about). Those would be CASCADE-dropped and not restored. MJ codegen-generated
115
+ * sprocs cover all standard CRUD pathways; bespoke sprocs against base views are
116
+ * extremely rare in practice. If a project does have them, they need to be re-applied
117
+ * after a 42P16 fallback fires.
118
+ *
119
+ * This pattern matches the v5.30.x fix migration `V202604282300` — which used the
120
+ * same DO/EXCEPTION construct to recreate `vwEntityPermissions` after the unquoted
121
+ * RoleName alias bug — proving the pattern is production-tested.
77
122
  *
78
123
  * Permissions are handled separately by sql_codegen.ts via generateViewPermissions().
79
124
  */
@@ -85,6 +130,17 @@ let PostgreSQLCodeGenProvider = class PostgreSQLCodeGenProvider extends CodeGenD
85
130
  const selectParts = this.buildBaseViewSelectParts(context, alias);
86
131
  const fromParts = this.buildBaseViewFromParts(context, entity, alias);
87
132
  const quotedView = pgDialect.QuoteSchema(entity.SchemaName, viewName);
133
+ const schemaLit = entity.SchemaName.replace(/'/g, "''");
134
+ const viewNameLit = viewName.replace(/'/g, "''");
135
+ // Inner CREATE OR REPLACE statement (no trailing semicolon — embedded inside
136
+ // the DO block via dollar-quoted literal).
137
+ const createOrReplaceSQL = `CREATE OR REPLACE VIEW ${quotedView}
138
+ AS
139
+ SELECT
140
+ ${selectParts}
141
+ FROM
142
+ ${pgDialect.QuoteSchema(entity.SchemaName, entity.BaseTable)} AS ${alias}${fromParts}
143
+ ${whereClause}`;
88
144
  return `
89
145
  ------------------------------------------------------------
90
146
  ----- BASE VIEW FOR ENTITY: ${entity.Name}
@@ -92,13 +148,185 @@ let PostgreSQLCodeGenProvider = class PostgreSQLCodeGenProvider extends CodeGenD
92
148
  ----- BASE TABLE: ${entity.BaseTable}
93
149
  ----- PRIMARY KEY: ${entity.PrimaryKeys.map((pk) => pk.Name).join(', ')}
94
150
  ------------------------------------------------------------
95
- CREATE OR REPLACE VIEW ${quotedView}
96
- AS
97
- SELECT
98
- ${selectParts}
99
- FROM
100
- ${pgDialect.QuoteSchema(entity.SchemaName, entity.BaseTable)} AS ${alias}${fromParts}
101
- ${whereClause};
151
+ DO $vw_regen$
152
+ DECLARE
153
+ vsql CONSTANT TEXT := $vsql$${createOrReplaceSQL}$vsql$;
154
+ rec RECORD;
155
+ BEGIN
156
+ EXECUTE vsql;
157
+ EXCEPTION WHEN invalid_table_definition THEN
158
+ -- 42P16: column rename/reorder/type change. CREATE OR REPLACE can't handle
159
+ -- non-additive shape changes — must DROP CASCADE + recreate. CASCADE drops
160
+ -- every dependent view (anything that JOINs this view in its body), so we
161
+ -- capture each dependent's definition + grants BEFORE the drop and replay
162
+ -- them afterward (best-effort). Without this, on a fresh-DB replay where
163
+ -- one entity's wrapper triggers (e.g. vwAIModelTypes shape changed since
164
+ -- baseline V202605021056), CASCADE wipes downstream views (vwAIModels)
165
+ -- that the wrapper for this entity doesn't know how to recreate, and
166
+ -- those views stay permanently missing.
167
+ CREATE TEMP TABLE IF NOT EXISTS _vw_regen_deps (
168
+ schema_name TEXT,
169
+ view_name TEXT,
170
+ relkind CHAR(1),
171
+ definition TEXT,
172
+ grants_sql TEXT
173
+ ) ON COMMIT DROP;
174
+ DELETE FROM _vw_regen_deps;
175
+
176
+ -- Capture dependent FUNCTIONS too. CASCADE drops every function with
177
+ -- RETURNS SETOF <view> (the codegen-emitted spCreate/spUpdate/spDelete
178
+ -- pattern) when the target view is dropped. Without restoring them,
179
+ -- post-codegen CRUD validation reports those routines as missing —
180
+ -- e.g. "MJ: Recommendation Items → missing create routine
181
+ -- spCreateRecommendationItem" — even though the next codegen pass
182
+ -- emits them. The restored definitions are pg_get_functiondef() output
183
+ -- which is a complete CREATE OR REPLACE FUNCTION statement plus a
184
+ -- trailing semicolon; replaying them verbatim recreates the function
185
+ -- with its original body, parameter list, and return type.
186
+ CREATE TEMP TABLE IF NOT EXISTS _vw_regen_fn_deps (
187
+ schema_name TEXT,
188
+ fn_name TEXT,
189
+ fn_oid OID,
190
+ definition TEXT
191
+ ) ON COMMIT DROP;
192
+ DELETE FROM _vw_regen_fn_deps;
193
+
194
+ -- Capture dependents. NOTES on the grants_sql build:
195
+ -- - Resolve role name via pg_get_userbyid(oid) — returns the bare,
196
+ -- unquoted role name (or 'unknown (OID=N)' if the oid no longer
197
+ -- exists). pg_get_userbyid is a public catalog function available to
198
+ -- every database user, including unprivileged accounts on managed
199
+ -- PostgreSQL services (Amazon RDS, Azure Database for PostgreSQL,
200
+ -- Cloud SQL) where pg_authid is restricted to the rds_superuser /
201
+ -- azure_pg_admin / cloudsqlsuperuser group. Earlier revisions joined
202
+ -- to pg_authid which works on self-hosted PG but fails with
203
+ -- "permission denied for table pg_authid" on managed services.
204
+ -- - The earlier (broken) approach cast (aclexplode).grantee::regrole::text
205
+ -- which RETURNS the role name pre-quoted when it contains uppercase
206
+ -- (e.g. cdp_Developer comes back already wrapped); calling quote_ident
207
+ -- on the already-quoted string double-wrapped and the GRANT failed at
208
+ -- replay with "role does not exist". Using
209
+ -- pg_get_userbyid returns a bare name and lets quote_ident wrap it
210
+ -- correctly exactly once.
211
+ -- - PUBLIC is grantee oid 0; pg_get_userbyid(0) returns 'unknown
212
+ -- (OID=0)' so handle the PUBLIC case explicitly and use it as the
213
+ -- literal 'PUBLIC' rather than quote_ident on the synthetic name.
214
+ INSERT INTO _vw_regen_deps (schema_name, view_name, relkind, definition, grants_sql)
215
+ SELECT DISTINCT
216
+ dn.nspname,
217
+ dc.relname,
218
+ dc.relkind,
219
+ pg_get_viewdef(dc.oid),
220
+ (SELECT string_agg(
221
+ 'GRANT ' || g.privilege || ' ON ' || quote_ident(dn.nspname) || '.' || quote_ident(dc.relname) ||
222
+ ' TO ' || (CASE WHEN g.grantee_oid = 0 THEN 'PUBLIC' ELSE quote_ident(pg_get_userbyid(g.grantee_oid)) END) || ';',
223
+ E'\n')
224
+ FROM (
225
+ SELECT (aclexplode(dc.relacl)).grantee AS grantee_oid,
226
+ (aclexplode(dc.relacl)).privilege_type AS privilege
227
+ ) g
228
+ WHERE g.privilege IN ('SELECT', 'INSERT', 'UPDATE', 'DELETE', 'TRUNCATE', 'REFERENCES', 'TRIGGER'))
229
+ FROM pg_depend d
230
+ JOIN pg_rewrite r ON r.oid = d.objid AND d.classid = 'pg_rewrite'::regclass
231
+ JOIN pg_class dc ON dc.oid = r.ev_class AND dc.relkind IN ('v', 'm')
232
+ JOIN pg_namespace dn ON dn.oid = dc.relnamespace
233
+ JOIN pg_class tc ON tc.oid = d.refobjid
234
+ JOIN pg_namespace tn ON tn.oid = tc.relnamespace
235
+ WHERE tn.nspname = '${schemaLit}'
236
+ AND tc.relname = '${viewNameLit}'
237
+ AND tc.relkind IN ('v', 'm')
238
+ AND dc.oid <> tc.oid;
239
+
240
+ -- Capture dependent functions. Two paths matter on PG:
241
+ -- 1. Functions whose RETURN type references the view (RETURNS SETOF
242
+ -- <view>) — pg_depend records this as type=pg_type → pg_class.
243
+ -- 2. Functions whose body references the view (used by sql functions
244
+ -- and by some plpgsql edge cases) — pg_depend records this as
245
+ -- pg_proc → pg_class.
246
+ -- pg_get_functiondef returns a complete CREATE OR REPLACE FUNCTION
247
+ -- statement that we replay verbatim. We DO include RETURNS-only
248
+ -- references because that's the dominant codegen pattern (sp* CRUD
249
+ -- functions all RETURNS SETOF the matching vwX).
250
+ INSERT INTO _vw_regen_fn_deps (schema_name, fn_name, fn_oid, definition)
251
+ SELECT DISTINCT
252
+ pn.nspname,
253
+ pp.proname,
254
+ pp.oid,
255
+ pg_get_functiondef(pp.oid)
256
+ FROM pg_depend d
257
+ JOIN pg_proc pp ON pp.oid = d.objid AND d.classid = 'pg_proc'::regclass
258
+ JOIN pg_namespace pn ON pn.oid = pp.pronamespace
259
+ JOIN pg_class tc ON tc.oid = d.refobjid
260
+ JOIN pg_namespace tn ON tn.oid = tc.relnamespace
261
+ WHERE tn.nspname = '${schemaLit}'
262
+ AND tc.relname = '${viewNameLit}'
263
+ AND tc.relkind IN ('v', 'm')
264
+ UNION
265
+ SELECT DISTINCT
266
+ pn.nspname,
267
+ pp.proname,
268
+ pp.oid,
269
+ pg_get_functiondef(pp.oid)
270
+ FROM pg_depend d
271
+ JOIN pg_type pt ON pt.oid = d.refobjid AND d.refclassid = 'pg_type'::regclass
272
+ JOIN pg_proc pp ON pp.prorettype = pt.oid OR pt.typrelid = pp.oid
273
+ JOIN pg_namespace pn ON pn.oid = pp.pronamespace
274
+ WHERE EXISTS (
275
+ SELECT 1 FROM pg_class tc
276
+ JOIN pg_namespace tn ON tn.oid = tc.relnamespace
277
+ WHERE tc.reltype = pt.oid
278
+ AND tn.nspname = '${schemaLit}'
279
+ AND tc.relname = '${viewNameLit}'
280
+ AND tc.relkind IN ('v', 'm')
281
+ );
282
+
283
+ DROP VIEW IF EXISTS ${quotedView} CASCADE;
284
+ EXECUTE vsql;
285
+
286
+ -- Replay captured dependents. Best-effort: log + continue on failure.
287
+ -- IMPORTANT: the CREATE VIEW and the GRANTs run in SEPARATE inner BEGIN
288
+ -- blocks. PL/pgSQL's BEGIN ... EXCEPTION creates an implicit savepoint
289
+ -- and rolls back EVERY statement in the block on any exception. If we
290
+ -- combined CREATE+GRANT in one block and a GRANT failed (e.g. role not
291
+ -- present in target environment), the just-recreated VIEW would also
292
+ -- get rolled back and stay missing — the exact failure mode this
293
+ -- wrapper exists to prevent.
294
+ FOR rec IN SELECT schema_name, view_name, relkind, definition, grants_sql FROM _vw_regen_deps LOOP
295
+ BEGIN
296
+ IF rec.relkind = 'm' THEN
297
+ EXECUTE 'CREATE MATERIALIZED VIEW ' || quote_ident(rec.schema_name) || '.' || quote_ident(rec.view_name) || ' AS ' || rec.definition;
298
+ ELSE
299
+ EXECUTE 'CREATE VIEW ' || quote_ident(rec.schema_name) || '.' || quote_ident(rec.view_name) || ' AS ' || rec.definition;
300
+ END IF;
301
+ EXCEPTION WHEN OTHERS THEN
302
+ RAISE NOTICE 'Best-effort restore skipped dependent %.%: %', rec.schema_name, rec.view_name, SQLERRM;
303
+ END;
304
+
305
+ IF rec.grants_sql IS NOT NULL THEN
306
+ BEGIN
307
+ EXECUTE rec.grants_sql;
308
+ EXCEPTION WHEN OTHERS THEN
309
+ RAISE NOTICE 'Best-effort grant restore skipped %.%: %', rec.schema_name, rec.view_name, SQLERRM;
310
+ END;
311
+ END IF;
312
+ END LOOP;
313
+
314
+ -- Replay captured dependent functions AFTER all dependent views are
315
+ -- restored — most codegen-emitted sp* functions reference both the
316
+ -- target view AND the dependent views in their bodies/return types.
317
+ -- Wrapped per-function in its own savepoint so a single failure
318
+ -- doesn't poison subsequent restores or the just-recreated target.
319
+ FOR rec IN SELECT schema_name, fn_name, definition FROM _vw_regen_fn_deps LOOP
320
+ BEGIN
321
+ EXECUTE rec.definition;
322
+ EXCEPTION WHEN OTHERS THEN
323
+ RAISE NOTICE 'Best-effort restore skipped dependent function %.%: %', rec.schema_name, rec.fn_name, SQLERRM;
324
+ END;
325
+ END LOOP;
326
+
327
+ DROP TABLE _vw_regen_deps;
328
+ DROP TABLE _vw_regen_fn_deps;
329
+ END $vw_regen$;
102
330
  `;
103
331
  }
104
332
  // ─── CRUD CREATE ─────────────────────────────────────────────────────
@@ -125,6 +353,225 @@ BEGIN
125
353
  END LOOP;
126
354
  END
127
355
  $do$;
356
+ `;
357
+ }
358
+ /**
359
+ * Returns true when this entity should emit JSON-arg sprocs (single `JSONB`
360
+ * parameter, key-presence semantics) for the given CRUD verb instead of the
361
+ * default typed-arg shape. Driven by PostgreSQL's hard 100-arg `FUNC_MAX_ARGS`
362
+ * ceiling — once a typed-arg sproc would project past `POSTGRESQL_PROCEDURE_PARAM_LIMIT`,
363
+ * we switch to JSON-arg.
364
+ *
365
+ * Calls the same `useJsonArgShape` predicate used by the runtime so codegen
366
+ * emit and runtime invocation cannot disagree (see `crudSprocFieldRules`
367
+ * in `@memberjunction/generic-database-provider`).
368
+ */
369
+ shouldUseJsonArgShape(entity, sprocType) {
370
+ return useJsonArgShape(entity, sprocType, POSTGRESQL_PROCEDURE_PARAM_LIMIT);
371
+ }
372
+ /**
373
+ * Returns the per-column JSON cast expression for the JSON-arg sproc body,
374
+ * e.g. `(p_data->>'PromptID')::UUID` or `(p_data->>'Status')` for plain text.
375
+ * The mapping mirrors `mapSQLType` but as JSON-extraction casts; types that
376
+ * don't need a cast (TEXT/VARCHAR) emit just the bare extraction. Binary
377
+ * (BYTEA) decodes from base64 string. JSON-typed columns use `->` (not
378
+ * `->>`) to keep the JSONB structure intact.
379
+ */
380
+ renderJsonExtractAndCast(field) {
381
+ const pgType = this.mapSQLType(field.SQLFullType).toUpperCase();
382
+ const fieldKey = `'${field.Name}'`;
383
+ // JSON / JSONB columns: keep structure via `p_data->'Field'`, no text cast.
384
+ if (pgType === 'JSONB' || pgType === 'JSON') {
385
+ return `(p_data->${fieldKey})::${pgType}`;
386
+ }
387
+ // Binary: caller serializes as base64-encoded string; sproc decodes.
388
+ if (pgType === 'BYTEA') {
389
+ return `decode(p_data->>${fieldKey}, 'base64')`;
390
+ }
391
+ // Plain text — no cast needed; PG returns TEXT from `->>`.
392
+ if (pgType === 'TEXT' || pgType.startsWith('VARCHAR') || pgType.startsWith('CHAR')) {
393
+ return `(p_data->>${fieldKey})`;
394
+ }
395
+ // Everything else (UUID, INTEGER, BIGINT, NUMERIC, BOOLEAN, TIMESTAMP[TZ], DATE, etc.)
396
+ // gets an explicit cast off the text extraction.
397
+ return `(p_data->>${fieldKey})::${pgType}`;
398
+ }
399
+ /**
400
+ * Generates the SET clause body for a JSON-arg UPDATE sproc — one `CASE WHEN
401
+ * p_data ? 'Field' THEN <cast> ELSE "Field" END` per writable column, plus
402
+ * the audit-timestamp assignment.
403
+ */
404
+ renderJsonArgUpdateSetClause(entity) {
405
+ const writableFields = entity.Fields.filter((f) => shouldIncludeFieldInParams(f, 'update') && !f.IsPrimaryKey);
406
+ const setClauses = writableFields.map((field) => {
407
+ const colName = pgDialect.QuoteIdentifier(field.Name);
408
+ const fieldKey = `'${field.Name}'`;
409
+ const cast = this.renderJsonExtractAndCast(field);
410
+ return `${colName} = CASE WHEN p_data ? ${fieldKey} THEN ${cast} ELSE ${colName} END`;
411
+ });
412
+ // __mj_UpdatedAt is always touched on UPDATE, never driven by p_data.
413
+ setClauses.push(`${pgDialect.QuoteIdentifier(EntityInfo.UpdatedAtFieldName)} = NOW()`);
414
+ return setClauses.join(',\n ');
415
+ }
416
+ /**
417
+ * Generates the JSON-arg variant of `spUpdate*` for wide entities. Single
418
+ * `p_data JSONB` parameter; per-column `CASE WHEN p_data ? 'Field' THEN
419
+ * <cast> ELSE "Field" END` body. Key-absent leaves the column unchanged;
420
+ * key=null clears it; key=value sets it. Identity column (`ID`) is required
421
+ * in the payload; all other columns are optional.
422
+ */
423
+ generateCRUDUpdateJsonArg(entity) {
424
+ const fnName = this.getCRUDRoutineName(entity, CRUDType.Update);
425
+ const viewName = this.getBaseViewName(entity);
426
+ const permissions = this.generateCRUDPermissions(entity, fnName, CRUDType.Update);
427
+ const setClause = this.renderJsonArgUpdateSetClause(entity);
428
+ const trigger = this.generateTimestampTrigger(entity);
429
+ // PK lookup. Composite-PK entities aren't expected to hit JSON-arg in
430
+ // practice (no current wide entity has a composite PK), but the shape
431
+ // still needs to handle them — extract each PK from p_data and AND the
432
+ // WHERE clause.
433
+ const pkExtractions = entity.PrimaryKeys.map((pk) => ` v_${this.toSnakeCase(pk.CodeName)} ${this.mapSQLType(pk.SQLFullType)} := (p_data->>'${pk.Name}')::${this.mapSQLType(pk.SQLFullType)};`).join('\n');
434
+ const pkValidation = entity.PrimaryKeys.map((pk) => `p_data ? '${pk.Name}'`).join(' AND ');
435
+ const whereClause = entity.PrimaryKeys.map((pk) => `${pgDialect.QuoteIdentifier(pk.Name)} = v_${this.toSnakeCase(pk.CodeName)}`).join(' AND ');
436
+ return `
437
+ ------------------------------------------------------------
438
+ ----- UPDATE FUNCTION FOR ${entity.BaseTable} (JSON-arg shape)
439
+ ------------------------------------------------------------
440
+ ${this.generateDropAllOverloadsBlock(entity.SchemaName, fnName)}
441
+ CREATE OR REPLACE FUNCTION ${pgDialect.QuoteSchema(entity.SchemaName, fnName)}(p_data JSONB)
442
+ RETURNS SETOF ${pgDialect.QuoteSchema(entity.SchemaName, viewName)}
443
+ AS $$
444
+ DECLARE
445
+ ${pkExtractions}
446
+ v_updated_count INTEGER;
447
+ BEGIN
448
+ IF p_data IS NULL OR NOT (${pkValidation}) THEN
449
+ RAISE EXCEPTION '${fnName}: p_data must include ${entity.PrimaryKeys.map((pk) => `"${pk.Name}"`).join(', ')}';
450
+ END IF;
451
+
452
+ UPDATE ${pgDialect.QuoteSchema(entity.SchemaName, entity.BaseTable)}
453
+ SET
454
+ ${setClause}
455
+ WHERE
456
+ ${whereClause};
457
+
458
+ GET DIAGNOSTICS v_updated_count = ROW_COUNT;
459
+
460
+ IF v_updated_count = 0 THEN
461
+ -- Nothing was updated, return empty result set
462
+ RETURN;
463
+ END IF;
464
+
465
+ -- Return the updated record from the base view
466
+ RETURN QUERY
467
+ SELECT * FROM ${pgDialect.QuoteSchema(entity.SchemaName, viewName)}
468
+ WHERE ${whereClause};
469
+ END;
470
+ $$ LANGUAGE plpgsql;
471
+ ${permissions}
472
+
473
+ ${trigger}
474
+ `;
475
+ }
476
+ /**
477
+ * Generates the JSON-arg variant of `spCreate*` for wide entities. Single
478
+ * `p_data JSONB` parameter; uses `EXECUTE format(...)` to build the INSERT
479
+ * dynamically based on which keys are present in `p_data` so absent keys
480
+ * fall back to column DEFAULT (matching the typed-arg sproc's default-
481
+ * substitution behavior). Returns the newly created row from the base view.
482
+ *
483
+ * Single-UUID-PK entities auto-generate the PK if `p_data` doesn't include
484
+ * one (matching `gen_random_uuid()` default). Composite-PK entities require
485
+ * caller-supplied PKs.
486
+ */
487
+ generateCRUDCreateJsonArg(entity) {
488
+ const fnName = this.getCRUDRoutineName(entity, CRUDType.Create);
489
+ const viewName = this.getBaseViewName(entity);
490
+ const permissions = this.generateCRUDPermissions(entity, fnName, CRUDType.Create);
491
+ const firstKey = entity.FirstPrimaryKey;
492
+ const pkType = firstKey.Type.toLowerCase().trim();
493
+ const pkIsUuidSingle = (pkType === 'uniqueidentifier' || pkType === 'uuid') && entity.PrimaryKeys.length === 1;
494
+ const pkPgType = this.mapSQLType(firstKey.SQLFullType);
495
+ const pkColQuoted = pgDialect.QuoteIdentifier(firstKey.Name);
496
+ // Writable fields participating in the INSERT — exclude PK (handled
497
+ // separately so we can supply a generated UUID when key is absent),
498
+ // exclude virtual / special-date / non-API-writable.
499
+ const writableFields = entity.Fields.filter((f) => shouldIncludeFieldInParams(f, 'create') && !f.IsPrimaryKey);
500
+ // Per-field cast tokens for the dynamic INSERT. The expressions reference
501
+ // `$1` (a positional parameter) instead of `p_data` because they end up
502
+ // inside a dynamic SQL string passed to `EXECUTE ... USING p_data`. Inside
503
+ // EXECUTE'd SQL only the bound positional parameters are visible — the
504
+ // outer function's local variables (including p_data) are NOT in scope.
505
+ const fieldCastEntries = writableFields
506
+ .map((f) => {
507
+ const cast = this.renderJsonExtractAndCast(f).replace(/p_data/g, '$1');
508
+ return ` WHEN '${f.Name}' THEN '${cast.replace(/'/g, "''")}'`;
509
+ })
510
+ .join('\n');
511
+ const fieldNamesArrayLiteral = writableFields.map((f) => `'${f.Name}'`).join(', ');
512
+ // ID resolution body: differs by PK strategy. Single-UUID PK is auto-
513
+ // generated when the caller doesn't supply one; everything else (composite,
514
+ // non-UUID) requires the caller to provide the key explicitly.
515
+ const idResolveBody = pkIsUuidSingle
516
+ ? ` IF p_data ? '${firstKey.Name}' THEN
517
+ v_id := (p_data->>'${firstKey.Name}')::${pkPgType};
518
+ ELSE
519
+ v_id := gen_random_uuid();
520
+ END IF;`
521
+ : ` IF NOT (p_data ? '${firstKey.Name}') THEN
522
+ RAISE EXCEPTION '${fnName}: p_data must include "${firstKey.Name}"';
523
+ END IF;
524
+ v_id := (p_data->>'${firstKey.Name}')::${pkPgType};`;
525
+ return `
526
+ ------------------------------------------------------------
527
+ ----- CREATE FUNCTION FOR ${entity.BaseTable} (JSON-arg shape)
528
+ ------------------------------------------------------------
529
+ ${this.generateDropAllOverloadsBlock(entity.SchemaName, fnName)}
530
+ CREATE OR REPLACE FUNCTION ${pgDialect.QuoteSchema(entity.SchemaName, fnName)}(p_data JSONB)
531
+ RETURNS SETOF ${pgDialect.QuoteSchema(entity.SchemaName, viewName)}
532
+ AS $$
533
+ DECLARE
534
+ v_id ${pkPgType};
535
+ v_field_name TEXT;
536
+ v_cast_expr TEXT;
537
+ v_col_list TEXT;
538
+ v_val_list TEXT;
539
+ v_sql TEXT;
540
+ BEGIN
541
+ ${idResolveBody}
542
+
543
+ v_col_list := quote_ident('${firstKey.Name.replace(/'/g, "''")}');
544
+ v_val_list := quote_literal(v_id) || '::${pkPgType}';
545
+
546
+ -- Build column / value lists from keys present in p_data. Absent keys are
547
+ -- omitted entirely so the column's DEFAULT applies (matching the typed-arg
548
+ -- sproc's default-substitution semantics).
549
+ FOREACH v_field_name IN ARRAY ARRAY[${fieldNamesArrayLiteral}]
550
+ LOOP
551
+ IF p_data ? v_field_name THEN
552
+ v_cast_expr := CASE v_field_name
553
+ ${fieldCastEntries}
554
+ END;
555
+ v_col_list := v_col_list || ', ' || quote_ident(v_field_name);
556
+ v_val_list := v_val_list || ', ' || v_cast_expr;
557
+ END IF;
558
+ END LOOP;
559
+
560
+ v_sql := format(
561
+ 'INSERT INTO ${pgDialect.QuoteSchema(entity.SchemaName, entity.BaseTable).replace(/'/g, "''")} (%s) VALUES (%s)',
562
+ v_col_list,
563
+ v_val_list
564
+ );
565
+ -- Pass p_data as a positional parameter so the cast expressions inside
566
+ -- v_val_list (which reference $1) can read the JSONB payload.
567
+ EXECUTE v_sql USING p_data;
568
+
569
+ RETURN QUERY
570
+ SELECT * FROM ${pgDialect.QuoteSchema(entity.SchemaName, viewName)}
571
+ WHERE ${pkColQuoted} = v_id;
572
+ END;
573
+ $$ LANGUAGE plpgsql;
574
+ ${permissions}
128
575
  `;
129
576
  }
130
577
  /**
@@ -137,8 +584,16 @@ $do$;
137
584
  *
138
585
  * Prepends a DROP-all-overloads block (see `generateDropAllOverloadsBlock`) so
139
586
  * adding/removing a column doesn't trigger PG's overload-ambiguity error.
587
+ *
588
+ * Wide entities (where `useJsonArgShape` returns true) emit a JSON-arg variant
589
+ * via `generateCRUDCreateJsonArg` — single `p_data JSONB` parameter, dynamic
590
+ * INSERT built from keys present in the payload. Same semantics; different
591
+ * wire shape needed because of PostgreSQL's 100-arg function ceiling.
140
592
  */
141
593
  generateCRUDCreate(entity) {
594
+ if (this.shouldUseJsonArgShape(entity, 'create')) {
595
+ return this.generateCRUDCreateJsonArg(entity);
596
+ }
142
597
  const fnName = this.getCRUDRoutineName(entity, CRUDType.Create);
143
598
  const viewName = this.getBaseViewName(entity);
144
599
  const paramString = this.generateCRUDParamString(entity.Fields, false);
@@ -192,6 +647,9 @@ ${permissions}
192
647
  * and emits `GRANT EXECUTE` permissions.
193
648
  */
194
649
  generateCRUDUpdate(entity) {
650
+ if (this.shouldUseJsonArgShape(entity, 'update')) {
651
+ return this.generateCRUDUpdateJsonArg(entity);
652
+ }
195
653
  const fnName = this.getCRUDRoutineName(entity, CRUDType.Update);
196
654
  const viewName = this.getBaseViewName(entity);
197
655
  const paramString = this.generateCRUDParamString(entity.Fields, true);
@@ -416,7 +874,7 @@ WHERE ${ftsColName} IS NULL;
416
874
  const primaryKey = entity.FirstPrimaryKey.Name;
417
875
  const primaryKeyType = this.mapSQLType(entity.FirstPrimaryKey.SQLFullType);
418
876
  const fieldName = field.Name;
419
- const fnName = `fn_${this.toSnakeCase(entity.BaseTable)}_${this.toSnakeCase(fieldName)}_get_root_id`;
877
+ const fnName = this.getRootIDFunctionName(entity, field);
420
878
  return `
421
879
  ------------------------------------------------------------
422
880
  ----- ROOT ID FUNCTION FOR: ${entity.BaseTable}.${fieldName}
@@ -460,6 +918,22 @@ CREATE OR REPLACE FUNCTION ${pgDialect.QuoteSchema(entity.SchemaName, fnName)}(
460
918
  $$ LANGUAGE sql STABLE;
461
919
  `;
462
920
  }
921
+ /**
922
+ * Produces the canonical name for the recursive root-finder helper function
923
+ * generated for self-referencing fields. The function definition and the
924
+ * view's LATERAL-JOIN reference must agree on this name (caller side calls
925
+ * via `generateRootFieldJoin`, definition via `generateRootIDFunction`).
926
+ *
927
+ * Note: this intentionally does NOT match the baseline-shipped PascalCase
928
+ * `fn{Table}{Field}_GetRootID` form, because the baseline returns
929
+ * `TABLE("RootID" type)` and the view callers expect a scalar — using the
930
+ * baseline name would clash with `cannot change return type of existing
931
+ * function`. The snake_case scalar form is codegen's own naming space and
932
+ * is consistent with how downstream views are emitted.
933
+ */
934
+ getRootIDFunctionName(entity, field) {
935
+ return `fn_${this.toSnakeCase(entity.BaseTable)}_${this.toSnakeCase(field.Name)}_get_root_id`;
936
+ }
463
937
  /** @inheritdoc */
464
938
  generateRootFieldSelect(_entity, field, alias) {
465
939
  const rootFieldName = `Root${field.Name}`;
@@ -471,7 +945,7 @@ $$ LANGUAGE sql STABLE;
471
945
  * `OUTER APPLY`) to call scalar functions inline within a view definition.
472
946
  */
473
947
  generateRootFieldJoin(entity, field, alias) {
474
- const fnName = `fn_${this.toSnakeCase(entity.BaseTable)}_${this.toSnakeCase(field.Name)}_get_root_id`;
948
+ const fnName = this.getRootIDFunctionName(entity, field);
475
949
  const tableAlias = entity.BaseTableCodeName.charAt(0).toLowerCase();
476
950
  return `LEFT JOIN LATERAL (
477
951
  SELECT ${pgDialect.QuoteSchema(entity.SchemaName, fnName)}(${tableAlias}.${pgDialect.QuoteIdentifier(entity.FirstPrimaryKey.Name)}, ${tableAlias}.${pgDialect.QuoteIdentifier(field.Name)}) AS root_id
@@ -585,8 +1059,14 @@ END $$;
585
1059
  continue;
586
1060
  // _Clear companion is emitted immediately before its main parameter
587
1061
  // for nullable columns whose database default is non-NULL.
1062
+ //
1063
+ // Type was historically `bit DEFAULT 0` (a copy-paste from the
1064
+ // SQL Server emitter). PG has no implicit cast from integer to
1065
+ // its `bit` type, and `bit` here was meant to mean a boolean
1066
+ // anyway — so the function compiles but every caller that passes
1067
+ // `0`/`false` fails type-checking. Emit native PG `boolean DEFAULT FALSE`.
588
1068
  if (!ef.IsPrimaryKey && this.needsClearCompanion(ef)) {
589
- parts.push(`${dialect.ParameterRef(ef.CodeName + '_Clear')} bit${dialect.ParameterDefault('0')}`);
1069
+ parts.push(`${dialect.ParameterRef(ef.CodeName + '_Clear')} ${dialect.BooleanParameterType()}${dialect.ParameterDefault(dialect.BooleanLiteral(false))}`);
590
1070
  foundDefault = true;
591
1071
  }
592
1072
  const isOptional = !this.isParamRequired(ef, isUpdate);
@@ -615,14 +1095,18 @@ END $$;
615
1095
  // ─── ROUTINE NAMING ──────────────────────────────────────────────────
616
1096
  /** @inheritdoc */
617
1097
  getCRUDRoutineName(entity, type) {
618
- const snakeTable = this.toSnakeCase(entity.BaseTableCodeName);
1098
+ // Match the baseline-ported `sp{Verb}{TableCodeName}` convention (SQL Server
1099
+ // names ported verbatim into PG). The runtime PostgreSQLDataProvider calls
1100
+ // these names directly — diverging here (e.g. `fn_create_<snake>`) means new
1101
+ // CodeGen runs leave functions the runtime can never find.
1102
+ const tableCodeName = entity.BaseTableCodeName;
619
1103
  switch (type) {
620
1104
  case CRUDType.Create:
621
- return entity.spCreate || `fn_create_${snakeTable}`;
1105
+ return entity.spCreate || `spCreate${tableCodeName}`;
622
1106
  case CRUDType.Update:
623
- return entity.spUpdate || `fn_update_${snakeTable}`;
1107
+ return entity.spUpdate || `spUpdate${tableCodeName}`;
624
1108
  case CRUDType.Delete:
625
- return entity.spDelete || `fn_delete_${snakeTable}`;
1109
+ return entity.spDelete || `spDelete${tableCodeName}`;
626
1110
  }
627
1111
  }
628
1112
  // ─── SQL HEADERS ─────────────────────────────────────────────────────
@@ -645,7 +1129,6 @@ END $$;
645
1129
  }
646
1130
  // ─── UTILITY ─────────────────────────────────────────────────────────
647
1131
  /**
648
- * Maps a SQL default value expression to its PostgreSQL equivalent. Translates
649
1132
  * SQL Server built-in functions (e.g., `NEWID()` to `gen_random_uuid()`,
650
1133
  * `GETUTCDATE()` to `NOW() AT TIME ZONE 'UTC'`), strips outer parentheses and
651
1134
  * surrounding single quotes, and re-applies quoting based on the {@link needsQuotes}
@@ -680,13 +1163,28 @@ END $$;
680
1163
  if (trimmedValue.startsWith('(') && trimmedValue.endsWith(')')) {
681
1164
  trimmedValue = trimmedValue.substring(1, trimmedValue.length - 1);
682
1165
  }
1166
+ // PG default values can come back as typed literals like
1167
+ // `'Pending'::character varying` or `'Active'::text`. The cast
1168
+ // suffix means the value is already a fully-formed PG expression —
1169
+ // stripping the leading-and-trailing quotes (line 14721 bug:
1170
+ // `'Pending'::character varying` → cleanValue = `Pending'::character varying`
1171
+ // → re-wrap → `'Pending'::character varying'` which is invalid SQL)
1172
+ // is wrong here. Detect this shape and pass through verbatim.
1173
+ if (/^'.*'::\w+(\s*\(\s*\d+\s*\))?(\s*\[\s*\])?$/.test(trimmedValue)) {
1174
+ return trimmedValue;
1175
+ }
683
1176
  // Remove surrounding quotes for clean value
684
1177
  let cleanValue = trimmedValue;
685
1178
  if (cleanValue.startsWith("'") && cleanValue.endsWith("'")) {
686
1179
  cleanValue = cleanValue.substring(1, cleanValue.length - 1);
687
1180
  }
688
- if (needsQuotes)
689
- return `'${cleanValue}'`;
1181
+ if (needsQuotes) {
1182
+ // Single-quote-escape the cleaned value (SQL standard: '' → ').
1183
+ // Without this, a default of `O'Brien` would re-emit as `'O'Brien'`
1184
+ // which closes the literal at the apostrophe and then fails parsing.
1185
+ const escaped = cleanValue.replace(/'/g, "''");
1186
+ return `'${escaped}'`;
1187
+ }
690
1188
  return cleanValue;
691
1189
  }
692
1190
  /**
@@ -752,7 +1250,15 @@ END $$;
752
1250
  addDefaultConstraintSQL(schema, tableName, columnName, defaultExpression) {
753
1251
  const table = pgDialect.QuoteSchema(schema, tableName);
754
1252
  const col = pgDialect.QuoteIdentifier(columnName);
755
- return `ALTER TABLE ${table} ALTER COLUMN ${col} SET DEFAULT ${defaultExpression}`;
1253
+ // Prepend SET CONSTRAINTS ALL IMMEDIATE so any deferred constraint
1254
+ // trigger events queued by earlier INSERTs in the same migration
1255
+ // transaction fire NOW, freeing the target table for ALTER. Without
1256
+ // this, mid-migration ALTER TABLE on a table that received earlier
1257
+ // INSERTs fails with `cannot ALTER TABLE because it has pending
1258
+ // trigger events` — codegen interleaves INSERTs (data sync) and
1259
+ // ALTER TABLE (default refresh) within a single Skyway-wrapped
1260
+ // transaction so the queue must be flushed at the boundary.
1261
+ return `SET CONSTRAINTS ALL IMMEDIATE;\nALTER TABLE ${table} ALTER COLUMN ${col} SET DEFAULT ${defaultExpression}`;
756
1262
  }
757
1263
  /**
758
1264
  * Generates a PL/pgSQL `DO $$` block that drops both a named CHECK constraint (if one
@@ -763,7 +1269,12 @@ END $$;
763
1269
  dropDefaultConstraintSQL(schema, tableName, columnName) {
764
1270
  const table = pgDialect.QuoteSchema(schema, tableName);
765
1271
  const col = pgDialect.QuoteIdentifier(columnName);
766
- return `
1272
+ // SET CONSTRAINTS ALL IMMEDIATE outside the DO block so it executes
1273
+ // first; any deferred trigger events from earlier INSERTs in the
1274
+ // same transaction fire NOW, clearing the queue before the ALTER
1275
+ // TABLE inside this DO block runs. See addDefaultConstraintSQL for
1276
+ // the rationale on why this is needed.
1277
+ return `SET CONSTRAINTS ALL IMMEDIATE;
767
1278
  DO $$
768
1279
  DECLARE
769
1280
  v_constraint_name TEXT;
@@ -807,7 +1318,7 @@ END $$`;
807
1318
  COALESCE(character_maximum_length, 0) AS "Length",
808
1319
  COALESCE(numeric_precision, 0) AS "Precision",
809
1320
  COALESCE(numeric_scale, 0) AS "Scale",
810
- CASE WHEN is_nullable = 'YES' THEN 1 ELSE 0 END AS "AllowsNull"
1321
+ CASE WHEN is_nullable = 'YES' THEN TRUE ELSE FALSE END AS "AllowsNull"
811
1322
  FROM information_schema.columns
812
1323
  WHERE table_schema = '${schema}'
813
1324
  AND table_name = '${viewName}'
@@ -890,6 +1401,11 @@ ORDER BY ordinal_position`;
890
1401
  // PL/pgSQL control flow
891
1402
  'NEW', 'OLD', 'FOUND', 'LOOP', 'WHILE', 'EXIT', 'CONTINUE',
892
1403
  'ELSIF', 'ELSEIF', 'STRICT',
1404
+ // Transaction / constraint control (used by SET CONSTRAINTS ALL IMMEDIATE
1405
+ // emitted before ALTER TABLE so deferred trigger events flush). Without
1406
+ // CONSTRAINTS / IMMEDIATE / DEFERRED in the keyword set, the tokenizer
1407
+ // double-quotes them as identifiers and PG rejects the resulting SQL.
1408
+ 'CONSTRAINTS', 'IMMEDIATE', 'DEFERRED', 'SAVEPOINT', 'RELEASE',
893
1409
  // SQL Server types
894
1410
  'NVARCHAR', 'VARCHAR', 'UNIQUEIDENTIFIER', 'DATETIMEOFFSET', 'DATETIME', 'DATETIME2',
895
1411
  'BIGINT', 'SMALLINT', 'TINYINT', 'FLOAT', 'REAL', 'DECIMAL', 'NUMERIC', 'MONEY',
@@ -1116,12 +1632,49 @@ WHERE p.prokind IN ('f', 'p')
1116
1632
  });
1117
1633
  await client.connect();
1118
1634
  try {
1635
+ // PG-only: emit recursive-FK root-ID helpers ahead of the view.
1636
+ //
1637
+ // PG codegen intentionally uses snake_case scalar
1638
+ // `fn_<table>_<field>_get_root_id` rather than the SS baseline's
1639
+ // PascalCase table-returning `fn<Table><Field>_GetRootID` — see
1640
+ // `getRootIDFunctionName`. The two names CAN'T coexist as the
1641
+ // same function (different return type), so codegen owns its
1642
+ // own snake_case namespace.
1643
+ //
1644
+ // Consequence: on a fresh PG database, the snake_case helpers
1645
+ // don't exist until codegen has run successfully at least once.
1646
+ // The `recompileAllBaseViews` → `regenerateFailedBaseViews`
1647
+ // recovery path used to call `generateBaseView` directly with
1648
+ // no helper emission, so customer first-run codegen against a
1649
+ // managed PG (where only baseline PascalCase functions exist)
1650
+ // hit `function fn_ai_agent_run_parent_run_id_get_root_id does
1651
+ // not exist` when LATERAL JOINs in the regenerated view body
1652
+ // resolved to the snake_case scalar form.
1653
+ //
1654
+ // SS doesn't have this problem because its codegen-generated
1655
+ // helper name matches the baseline name — the baseline always
1656
+ // satisfies the LATERAL JOIN. Hence the fix lives here, not
1657
+ // in the dialect-agnostic recovery loop.
1658
+ const recursiveFKs = entity.Fields.filter(f => f.RelatedEntityID != null && UUIDsEqual(f.RelatedEntityID, entity.ID));
1659
+ for (const field of recursiveFKs) {
1660
+ const fnSQL = this.generateRootIDFunction(entity, field);
1661
+ if (fnSQL && fnSQL.trim().length > 0) {
1662
+ await client.query(fnSQL);
1663
+ }
1664
+ }
1119
1665
  await executeWithFallback({
1120
1666
  client,
1121
1667
  schema: entity.SchemaName,
1122
1668
  viewName: entity.BaseView,
1123
1669
  createOrReplaceSQL: viewSQL,
1124
1670
  willRegenerate,
1671
+ // Pass the base table so viewFallback can materialize a stub
1672
+ // first if the view body has a self-reference and the view
1673
+ // doesn't yet exist (e.g. vwRecordChanges joins to itself for
1674
+ // parent lookup; if it was CASCADE-dropped earlier in the
1675
+ // same codegen run, CREATE OR REPLACE can't resolve the
1676
+ // self-reference until a placeholder exists).
1677
+ baseTableQualified: pgDialect.QuoteSchema(entity.SchemaName, entity.BaseTable),
1125
1678
  });
1126
1679
  }
1127
1680
  finally {
@@ -1189,6 +1742,7 @@ WHERE p.prokind IN ('f', 'p')
1189
1742
  viewName: opts.entity.BaseView,
1190
1743
  createOrReplaceSQL: opts.viewSQL,
1191
1744
  willRegenerate: opts.willRegenerate,
1745
+ baseTableQualified: pgDialect.QuoteSchema(opts.entity.SchemaName, opts.entity.BaseTable),
1192
1746
  });
1193
1747
  }
1194
1748
  catch (e) {
@@ -1676,16 +2230,17 @@ numbered_rows AS (
1676
2230
  sf."AutoIncrement",
1677
2231
  ${this.buildAllowUpdateAPICase()},
1678
2232
  sf."IsVirtual",
2233
+ sf."IsComputed",
1679
2234
  e."RelationshipDefaultDisplayType",
1680
2235
  e."Name" AS "EntityName",
1681
2236
  re."ID" AS "RelatedEntityID",
1682
2237
  fk."referenced_column" AS "RelatedEntityFieldName",
1683
- CASE WHEN sf."FieldName" = 'Name' THEN 1 ELSE 0 END AS "IsNameField",
1684
- CASE WHEN pk."ColumnName" IS NOT NULL THEN 1 ELSE 0 END AS "IsPrimaryKey",
2238
+ CASE WHEN sf."FieldName" = 'Name' THEN TRUE ELSE FALSE END AS "IsNameField",
2239
+ CASE WHEN pk."ColumnName" IS NOT NULL THEN TRUE ELSE FALSE END AS "IsPrimaryKey",
1685
2240
  CASE
1686
- WHEN pk."ColumnName" IS NOT NULL THEN 1
1687
- WHEN uk."ColumnName" IS NOT NULL THEN 1
1688
- ELSE 0
2241
+ WHEN pk."ColumnName" IS NOT NULL THEN TRUE
2242
+ WHEN uk."ColumnName" IS NOT NULL THEN TRUE
2243
+ ELSE FALSE
1689
2244
  END AS "IsUnique",
1690
2245
  ROW_NUMBER() OVER (PARTITION BY sf."EntityID", sf."FieldName" ORDER BY (SELECT NULL)) AS rn
1691
2246
  FROM
@@ -1716,12 +2271,12 @@ ORDER BY "EntityID", "Sequence";
1716
2271
  * Builds the CASE expression for AllowUpdateAPI in the pending entity fields query.
1717
2272
  */
1718
2273
  buildAllowUpdateAPICase() {
1719
- return `CASE WHEN sf."IsVirtual" = true THEN 0
1720
- WHEN sf."FieldName" = '${EntityInfo.CreatedAtFieldName}' THEN 0
1721
- WHEN sf."FieldName" = '${EntityInfo.UpdatedAtFieldName}' THEN 0
1722
- WHEN sf."FieldName" = '${EntityInfo.DeletedAtFieldName}' THEN 0
1723
- WHEN pk."ColumnName" IS NOT NULL THEN 0
1724
- ELSE 1
2274
+ return `CASE WHEN sf."IsVirtual" = true THEN FALSE
2275
+ WHEN sf."FieldName" = '${EntityInfo.CreatedAtFieldName}' THEN FALSE
2276
+ WHEN sf."FieldName" = '${EntityInfo.UpdatedAtFieldName}' THEN FALSE
2277
+ WHEN sf."FieldName" = '${EntityInfo.DeletedAtFieldName}' THEN FALSE
2278
+ WHEN pk."ColumnName" IS NOT NULL THEN FALSE
2279
+ ELSE TRUE
1725
2280
  END AS "AllowUpdateAPI"`;
1726
2281
  }
1727
2282
  /**
@@ -1748,11 +2303,12 @@ WHERE vf."IsVirtual" = true
1748
2303
  AND fk."RelatedEntityNameFieldMap" != ''
1749
2304
  AND LOWER(vf."Name") = LOWER(fk."RelatedEntityNameFieldMap"))
1750
2305
  )
1751
- AND vf."AllowsNull" != fk."AllowsNull"`;
2306
+ AND vf."AllowsNull" != fk."AllowsNull";
2307
+ `;
1752
2308
  }
1753
2309
  };
1754
2310
  PostgreSQLCodeGenProvider = PostgreSQLCodeGenProvider_1 = __decorate([
1755
- RegisterClass(CodeGenDatabaseProvider, 'PostgreSQLCodeGenProvider')
2311
+ RegisterClass(CodeGenDatabaseProvider, 'postgresql')
1756
2312
  ], PostgreSQLCodeGenProvider);
1757
2313
  export { PostgreSQLCodeGenProvider };
1758
2314
  //# sourceMappingURL=PostgreSQLCodeGenProvider.js.map