@memberjunction/codegen-lib 5.31.0 → 5.33.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 (59) 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 +53 -0
  10. package/dist/Database/codeGenDatabaseProvider.d.ts.map +1 -1
  11. package/dist/Database/codeGenDatabaseProvider.js +66 -7
  12. package/dist/Database/codeGenDatabaseProvider.js.map +1 -1
  13. package/dist/Database/manage-metadata.d.ts +81 -4
  14. package/dist/Database/manage-metadata.d.ts.map +1 -1
  15. package/dist/Database/manage-metadata.js +245 -22
  16. package/dist/Database/manage-metadata.js.map +1 -1
  17. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts +131 -20
  18. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts.map +1 -1
  19. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js +640 -53
  20. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js.map +1 -1
  21. package/dist/Database/providers/postgresql/viewFallback.d.ts +14 -0
  22. package/dist/Database/providers/postgresql/viewFallback.d.ts.map +1 -1
  23. package/dist/Database/providers/postgresql/viewFallback.js +107 -13
  24. package/dist/Database/providers/postgresql/viewFallback.js.map +1 -1
  25. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts +5 -2
  26. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts.map +1 -1
  27. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js +26 -7
  28. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js.map +1 -1
  29. package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.test.js +40 -9
  30. package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.test.js.map +1 -1
  31. package/dist/Database/sql.d.ts +8 -0
  32. package/dist/Database/sql.d.ts.map +1 -1
  33. package/dist/Database/sql.js +18 -14
  34. package/dist/Database/sql.js.map +1 -1
  35. package/dist/Database/sql_codegen.d.ts +18 -5
  36. package/dist/Database/sql_codegen.d.ts.map +1 -1
  37. package/dist/Database/sql_codegen.js +83 -48
  38. package/dist/Database/sql_codegen.js.map +1 -1
  39. package/dist/Misc/action_subclasses_codegen.js +1 -1
  40. package/dist/Misc/action_subclasses_codegen.js.map +1 -1
  41. package/dist/Misc/createNewUser.d.ts.map +1 -1
  42. package/dist/Misc/createNewUser.js +14 -3
  43. package/dist/Misc/createNewUser.js.map +1 -1
  44. package/dist/Misc/graphql_server_codegen.d.ts +11 -0
  45. package/dist/Misc/graphql_server_codegen.d.ts.map +1 -1
  46. package/dist/Misc/graphql_server_codegen.js +27 -40
  47. package/dist/Misc/graphql_server_codegen.js.map +1 -1
  48. package/dist/Misc/sql_logging.d.ts.map +1 -1
  49. package/dist/Misc/sql_logging.js +16 -2
  50. package/dist/Misc/sql_logging.js.map +1 -1
  51. package/dist/Misc/system_integrity.js +3 -3
  52. package/dist/Misc/system_integrity.js.map +1 -1
  53. package/dist/Misc/temp_batch_file.js +2 -2
  54. package/dist/Misc/temp_batch_file.js.map +1 -1
  55. package/dist/runCodeGen.d.ts +1 -1
  56. package/dist/runCodeGen.d.ts.map +1 -1
  57. package/dist/runCodeGen.js +51 -6
  58. package/dist/runCodeGen.js.map +1 -1
  59. 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; }
@@ -55,25 +61,43 @@ let PostgreSQLCodeGenProvider = class PostgreSQLCodeGenProvider extends CodeGenD
55
61
  }
56
62
  // ─── BASE VIEWS ──────────────────────────────────────────────────────
57
63
  /**
58
- * Generates a PostgreSQL `CREATE OR REPLACE VIEW` statement for an entity's base view.
64
+ * Generates a PostgreSQL view-regeneration block for an entity's base view.
65
+ *
59
66
  * Includes all base table columns, parent/related field joins, and root field lateral
60
67
  * joins. Applies a soft-delete `WHERE` filter when the entity uses soft deletes.
61
68
  *
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.
69
+ * **Two-path emission (try-then-fallback).** The output wraps `CREATE OR REPLACE VIEW`
70
+ * in a `DO $$ ... EXCEPTION WHEN invalid_table_definition THEN DROP VIEW ... CASCADE;
71
+ * EXECUTE vsql; END $$` block. Why:
72
+ *
73
+ * - Happy path: `CREATE OR REPLACE VIEW` succeeds (new column list is a prefix of
74
+ * the existing one plus optional trailing additions). Zero destruction. No
75
+ * dependent views, functions, triggers, or grants are touched.
76
+ *
77
+ * - Sad path: PG raises SQLSTATE `42P16 invalid_table_definition` for any column
78
+ * rename / reorder / type change / removal. The exception handler runs
79
+ * `DROP VIEW ... CASCADE` and re-executes the CREATE. Dependent codegen-managed
80
+ * functions (spCreate/spUpdate/spDelete returning `SETOF vwFoo`) and dependent
81
+ * views are CASCADE-dropped — they are regenerated later in the same codegen
82
+ * output stream, so by the end of the run all dependents are restored to the new
83
+ * shape. GRANTs on the view itself are also lost on the CASCADE; codegen always
84
+ * re-emits permissions immediately after the view, so they come back too.
85
+ *
86
+ * The runtime-apply path also calls this, so the live DB applies the same DO block.
87
+ * `executeWithFallback` (the runtime helper) becomes a no-op for these statements
88
+ * because the DO block handles 42P16 internally — but it still runs as a safety net
89
+ * for any other failure modes.
67
90
  *
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.
91
+ * **What this DOES NOT preserve on the sad path:** non-codegen-managed dependent
92
+ * objects (e.g. a hand-written sproc against this view that codegen doesn't know
93
+ * about). Those would be CASCADE-dropped and not restored. MJ codegen-generated
94
+ * sprocs cover all standard CRUD pathways; bespoke sprocs against base views are
95
+ * extremely rare in practice. If a project does have them, they need to be re-applied
96
+ * after a 42P16 fallback fires.
97
+ *
98
+ * This pattern matches the v5.30.x fix migration `V202604282300` — which used the
99
+ * same DO/EXCEPTION construct to recreate `vwEntityPermissions` after the unquoted
100
+ * RoleName alias bug — proving the pattern is production-tested.
77
101
  *
78
102
  * Permissions are handled separately by sql_codegen.ts via generateViewPermissions().
79
103
  */
@@ -85,6 +109,17 @@ let PostgreSQLCodeGenProvider = class PostgreSQLCodeGenProvider extends CodeGenD
85
109
  const selectParts = this.buildBaseViewSelectParts(context, alias);
86
110
  const fromParts = this.buildBaseViewFromParts(context, entity, alias);
87
111
  const quotedView = pgDialect.QuoteSchema(entity.SchemaName, viewName);
112
+ const schemaLit = entity.SchemaName.replace(/'/g, "''");
113
+ const viewNameLit = viewName.replace(/'/g, "''");
114
+ // Inner CREATE OR REPLACE statement (no trailing semicolon — embedded inside
115
+ // the DO block via dollar-quoted literal).
116
+ const createOrReplaceSQL = `CREATE OR REPLACE VIEW ${quotedView}
117
+ AS
118
+ SELECT
119
+ ${selectParts}
120
+ FROM
121
+ ${pgDialect.QuoteSchema(entity.SchemaName, entity.BaseTable)} AS ${alias}${fromParts}
122
+ ${whereClause}`;
88
123
  return `
89
124
  ------------------------------------------------------------
90
125
  ----- BASE VIEW FOR ENTITY: ${entity.Name}
@@ -92,13 +127,185 @@ let PostgreSQLCodeGenProvider = class PostgreSQLCodeGenProvider extends CodeGenD
92
127
  ----- BASE TABLE: ${entity.BaseTable}
93
128
  ----- PRIMARY KEY: ${entity.PrimaryKeys.map((pk) => pk.Name).join(', ')}
94
129
  ------------------------------------------------------------
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};
130
+ DO $vw_regen$
131
+ DECLARE
132
+ vsql CONSTANT TEXT := $vsql$${createOrReplaceSQL}$vsql$;
133
+ rec RECORD;
134
+ BEGIN
135
+ EXECUTE vsql;
136
+ EXCEPTION WHEN invalid_table_definition THEN
137
+ -- 42P16: column rename/reorder/type change. CREATE OR REPLACE can't handle
138
+ -- non-additive shape changes — must DROP CASCADE + recreate. CASCADE drops
139
+ -- every dependent view (anything that JOINs this view in its body), so we
140
+ -- capture each dependent's definition + grants BEFORE the drop and replay
141
+ -- them afterward (best-effort). Without this, on a fresh-DB replay where
142
+ -- one entity's wrapper triggers (e.g. vwAIModelTypes shape changed since
143
+ -- baseline V202605021056), CASCADE wipes downstream views (vwAIModels)
144
+ -- that the wrapper for this entity doesn't know how to recreate, and
145
+ -- those views stay permanently missing.
146
+ CREATE TEMP TABLE IF NOT EXISTS _vw_regen_deps (
147
+ schema_name TEXT,
148
+ view_name TEXT,
149
+ relkind CHAR(1),
150
+ definition TEXT,
151
+ grants_sql TEXT
152
+ ) ON COMMIT DROP;
153
+ DELETE FROM _vw_regen_deps;
154
+
155
+ -- Capture dependent FUNCTIONS too. CASCADE drops every function with
156
+ -- RETURNS SETOF <view> (the codegen-emitted spCreate/spUpdate/spDelete
157
+ -- pattern) when the target view is dropped. Without restoring them,
158
+ -- post-codegen CRUD validation reports those routines as missing —
159
+ -- e.g. "MJ: Recommendation Items → missing create routine
160
+ -- spCreateRecommendationItem" — even though the next codegen pass
161
+ -- emits them. The restored definitions are pg_get_functiondef() output
162
+ -- which is a complete CREATE OR REPLACE FUNCTION statement plus a
163
+ -- trailing semicolon; replaying them verbatim recreates the function
164
+ -- with its original body, parameter list, and return type.
165
+ CREATE TEMP TABLE IF NOT EXISTS _vw_regen_fn_deps (
166
+ schema_name TEXT,
167
+ fn_name TEXT,
168
+ fn_oid OID,
169
+ definition TEXT
170
+ ) ON COMMIT DROP;
171
+ DELETE FROM _vw_regen_fn_deps;
172
+
173
+ -- Capture dependents. NOTES on the grants_sql build:
174
+ -- - Resolve role name via pg_get_userbyid(oid) — returns the bare,
175
+ -- unquoted role name (or 'unknown (OID=N)' if the oid no longer
176
+ -- exists). pg_get_userbyid is a public catalog function available to
177
+ -- every database user, including unprivileged accounts on managed
178
+ -- PostgreSQL services (Amazon RDS, Azure Database for PostgreSQL,
179
+ -- Cloud SQL) where pg_authid is restricted to the rds_superuser /
180
+ -- azure_pg_admin / cloudsqlsuperuser group. Earlier revisions joined
181
+ -- to pg_authid which works on self-hosted PG but fails with
182
+ -- "permission denied for table pg_authid" on managed services.
183
+ -- - The earlier (broken) approach cast (aclexplode).grantee::regrole::text
184
+ -- which RETURNS the role name pre-quoted when it contains uppercase
185
+ -- (e.g. cdp_Developer comes back already wrapped); calling quote_ident
186
+ -- on the already-quoted string double-wrapped and the GRANT failed at
187
+ -- replay with "role does not exist". Using
188
+ -- pg_get_userbyid returns a bare name and lets quote_ident wrap it
189
+ -- correctly exactly once.
190
+ -- - PUBLIC is grantee oid 0; pg_get_userbyid(0) returns 'unknown
191
+ -- (OID=0)' so handle the PUBLIC case explicitly and use it as the
192
+ -- literal 'PUBLIC' rather than quote_ident on the synthetic name.
193
+ INSERT INTO _vw_regen_deps (schema_name, view_name, relkind, definition, grants_sql)
194
+ SELECT DISTINCT
195
+ dn.nspname,
196
+ dc.relname,
197
+ dc.relkind,
198
+ pg_get_viewdef(dc.oid),
199
+ (SELECT string_agg(
200
+ 'GRANT ' || g.privilege || ' ON ' || quote_ident(dn.nspname) || '.' || quote_ident(dc.relname) ||
201
+ ' TO ' || (CASE WHEN g.grantee_oid = 0 THEN 'PUBLIC' ELSE quote_ident(pg_get_userbyid(g.grantee_oid)) END) || ';',
202
+ E'\n')
203
+ FROM (
204
+ SELECT (aclexplode(dc.relacl)).grantee AS grantee_oid,
205
+ (aclexplode(dc.relacl)).privilege_type AS privilege
206
+ ) g
207
+ WHERE g.privilege IN ('SELECT', 'INSERT', 'UPDATE', 'DELETE', 'TRUNCATE', 'REFERENCES', 'TRIGGER'))
208
+ FROM pg_depend d
209
+ JOIN pg_rewrite r ON r.oid = d.objid AND d.classid = 'pg_rewrite'::regclass
210
+ JOIN pg_class dc ON dc.oid = r.ev_class AND dc.relkind IN ('v', 'm')
211
+ JOIN pg_namespace dn ON dn.oid = dc.relnamespace
212
+ JOIN pg_class tc ON tc.oid = d.refobjid
213
+ JOIN pg_namespace tn ON tn.oid = tc.relnamespace
214
+ WHERE tn.nspname = '${schemaLit}'
215
+ AND tc.relname = '${viewNameLit}'
216
+ AND tc.relkind IN ('v', 'm')
217
+ AND dc.oid <> tc.oid;
218
+
219
+ -- Capture dependent functions. Two paths matter on PG:
220
+ -- 1. Functions whose RETURN type references the view (RETURNS SETOF
221
+ -- <view>) — pg_depend records this as type=pg_type → pg_class.
222
+ -- 2. Functions whose body references the view (used by sql functions
223
+ -- and by some plpgsql edge cases) — pg_depend records this as
224
+ -- pg_proc → pg_class.
225
+ -- pg_get_functiondef returns a complete CREATE OR REPLACE FUNCTION
226
+ -- statement that we replay verbatim. We DO include RETURNS-only
227
+ -- references because that's the dominant codegen pattern (sp* CRUD
228
+ -- functions all RETURNS SETOF the matching vwX).
229
+ INSERT INTO _vw_regen_fn_deps (schema_name, fn_name, fn_oid, definition)
230
+ SELECT DISTINCT
231
+ pn.nspname,
232
+ pp.proname,
233
+ pp.oid,
234
+ pg_get_functiondef(pp.oid)
235
+ FROM pg_depend d
236
+ JOIN pg_proc pp ON pp.oid = d.objid AND d.classid = 'pg_proc'::regclass
237
+ JOIN pg_namespace pn ON pn.oid = pp.pronamespace
238
+ JOIN pg_class tc ON tc.oid = d.refobjid
239
+ JOIN pg_namespace tn ON tn.oid = tc.relnamespace
240
+ WHERE tn.nspname = '${schemaLit}'
241
+ AND tc.relname = '${viewNameLit}'
242
+ AND tc.relkind IN ('v', 'm')
243
+ UNION
244
+ SELECT DISTINCT
245
+ pn.nspname,
246
+ pp.proname,
247
+ pp.oid,
248
+ pg_get_functiondef(pp.oid)
249
+ FROM pg_depend d
250
+ JOIN pg_type pt ON pt.oid = d.refobjid AND d.refclassid = 'pg_type'::regclass
251
+ JOIN pg_proc pp ON pp.prorettype = pt.oid OR pt.typrelid = pp.oid
252
+ JOIN pg_namespace pn ON pn.oid = pp.pronamespace
253
+ WHERE EXISTS (
254
+ SELECT 1 FROM pg_class tc
255
+ JOIN pg_namespace tn ON tn.oid = tc.relnamespace
256
+ WHERE tc.reltype = pt.oid
257
+ AND tn.nspname = '${schemaLit}'
258
+ AND tc.relname = '${viewNameLit}'
259
+ AND tc.relkind IN ('v', 'm')
260
+ );
261
+
262
+ DROP VIEW IF EXISTS ${quotedView} CASCADE;
263
+ EXECUTE vsql;
264
+
265
+ -- Replay captured dependents. Best-effort: log + continue on failure.
266
+ -- IMPORTANT: the CREATE VIEW and the GRANTs run in SEPARATE inner BEGIN
267
+ -- blocks. PL/pgSQL's BEGIN ... EXCEPTION creates an implicit savepoint
268
+ -- and rolls back EVERY statement in the block on any exception. If we
269
+ -- combined CREATE+GRANT in one block and a GRANT failed (e.g. role not
270
+ -- present in target environment), the just-recreated VIEW would also
271
+ -- get rolled back and stay missing — the exact failure mode this
272
+ -- wrapper exists to prevent.
273
+ FOR rec IN SELECT schema_name, view_name, relkind, definition, grants_sql FROM _vw_regen_deps LOOP
274
+ BEGIN
275
+ IF rec.relkind = 'm' THEN
276
+ EXECUTE 'CREATE MATERIALIZED VIEW ' || quote_ident(rec.schema_name) || '.' || quote_ident(rec.view_name) || ' AS ' || rec.definition;
277
+ ELSE
278
+ EXECUTE 'CREATE VIEW ' || quote_ident(rec.schema_name) || '.' || quote_ident(rec.view_name) || ' AS ' || rec.definition;
279
+ END IF;
280
+ EXCEPTION WHEN OTHERS THEN
281
+ RAISE NOTICE 'Best-effort restore skipped dependent %.%: %', rec.schema_name, rec.view_name, SQLERRM;
282
+ END;
283
+
284
+ IF rec.grants_sql IS NOT NULL THEN
285
+ BEGIN
286
+ EXECUTE rec.grants_sql;
287
+ EXCEPTION WHEN OTHERS THEN
288
+ RAISE NOTICE 'Best-effort grant restore skipped %.%: %', rec.schema_name, rec.view_name, SQLERRM;
289
+ END;
290
+ END IF;
291
+ END LOOP;
292
+
293
+ -- Replay captured dependent functions AFTER all dependent views are
294
+ -- restored — most codegen-emitted sp* functions reference both the
295
+ -- target view AND the dependent views in their bodies/return types.
296
+ -- Wrapped per-function in its own savepoint so a single failure
297
+ -- doesn't poison subsequent restores or the just-recreated target.
298
+ FOR rec IN SELECT schema_name, fn_name, definition FROM _vw_regen_fn_deps LOOP
299
+ BEGIN
300
+ EXECUTE rec.definition;
301
+ EXCEPTION WHEN OTHERS THEN
302
+ RAISE NOTICE 'Best-effort restore skipped dependent function %.%: %', rec.schema_name, rec.fn_name, SQLERRM;
303
+ END;
304
+ END LOOP;
305
+
306
+ DROP TABLE _vw_regen_deps;
307
+ DROP TABLE _vw_regen_fn_deps;
308
+ END $vw_regen$;
102
309
  `;
103
310
  }
104
311
  // ─── CRUD CREATE ─────────────────────────────────────────────────────
@@ -125,6 +332,234 @@ BEGIN
125
332
  END LOOP;
126
333
  END
127
334
  $do$;
335
+ `;
336
+ }
337
+ /**
338
+ * Returns true when this entity should emit JSON-arg sprocs (single `JSONB`
339
+ * parameter, key-presence semantics) for the given CRUD verb instead of the
340
+ * default typed-arg shape. Driven by PostgreSQL's hard 100-arg `FUNC_MAX_ARGS`
341
+ * ceiling — once a typed-arg sproc would project past `POSTGRESQL_PROCEDURE_PARAM_LIMIT`,
342
+ * we switch to JSON-arg.
343
+ *
344
+ * Calls the same `useJsonArgShape` predicate used by the runtime so codegen
345
+ * emit and runtime invocation cannot disagree (see `crudSprocFieldRules`
346
+ * in `@memberjunction/generic-database-provider`).
347
+ */
348
+ shouldUseJsonArgShape(entity, sprocType) {
349
+ return useJsonArgShape(entity, sprocType, POSTGRESQL_PROCEDURE_PARAM_LIMIT);
350
+ }
351
+ /**
352
+ * Returns the per-column JSON cast expression for the JSON-arg sproc body,
353
+ * e.g. `(p_data->>'PromptID')::UUID` or `(p_data->>'Status')` for plain text.
354
+ * The mapping mirrors `mapSQLType` but as JSON-extraction casts; types that
355
+ * don't need a cast (TEXT/VARCHAR) emit just the bare extraction. Binary
356
+ * (BYTEA) decodes from base64 string. JSON-typed columns use `->` (not
357
+ * `->>`) to keep the JSONB structure intact.
358
+ */
359
+ renderJsonExtractAndCast(field) {
360
+ const pgType = this.mapSQLType(field.SQLFullType).toUpperCase();
361
+ const fieldKey = `'${field.Name}'`;
362
+ // JSON / JSONB columns: keep structure via `p_data->'Field'`, no text cast.
363
+ if (pgType === 'JSONB' || pgType === 'JSON') {
364
+ return `(p_data->${fieldKey})::${pgType}`;
365
+ }
366
+ // Binary: caller serializes as base64-encoded string; sproc decodes.
367
+ if (pgType === 'BYTEA') {
368
+ return `decode(p_data->>${fieldKey}, 'base64')`;
369
+ }
370
+ // Plain text — no cast needed; PG returns TEXT from `->>`.
371
+ if (pgType === 'TEXT' || pgType.startsWith('VARCHAR') || pgType.startsWith('CHAR')) {
372
+ return `(p_data->>${fieldKey})`;
373
+ }
374
+ // Everything else (UUID, INTEGER, BIGINT, NUMERIC, BOOLEAN, TIMESTAMP[TZ], DATE, etc.)
375
+ // gets an explicit cast off the text extraction.
376
+ return `(p_data->>${fieldKey})::${pgType}`;
377
+ }
378
+ /**
379
+ * Generates the SET clause body for a JSON-arg UPDATE sproc — one `CASE WHEN
380
+ * p_data ? 'Field' THEN <cast> ELSE "Field" END` per writable column, plus
381
+ * the audit-timestamp assignment.
382
+ */
383
+ renderJsonArgUpdateSetClause(entity) {
384
+ const writableFields = entity.Fields.filter((f) => shouldIncludeFieldInParams(f, 'update') && !f.IsPrimaryKey);
385
+ const setClauses = writableFields.map((field) => {
386
+ const colName = pgDialect.QuoteIdentifier(field.Name);
387
+ const fieldKey = `'${field.Name}'`;
388
+ const cast = this.renderJsonExtractAndCast(field);
389
+ return `${colName} = CASE WHEN p_data ? ${fieldKey} THEN ${cast} ELSE ${colName} END`;
390
+ });
391
+ // __mj_UpdatedAt is always touched on UPDATE, never driven by p_data.
392
+ setClauses.push(`${pgDialect.QuoteIdentifier(EntityInfo.UpdatedAtFieldName)} = NOW()`);
393
+ return setClauses.join(',\n ');
394
+ }
395
+ /**
396
+ * Generates the JSON-arg variant of `spUpdate*` for wide entities. Single
397
+ * `p_data JSONB` parameter; per-column `CASE WHEN p_data ? 'Field' THEN
398
+ * <cast> ELSE "Field" END` body. Key-absent leaves the column unchanged;
399
+ * key=null clears it; key=value sets it. Identity column (`ID`) is required
400
+ * in the payload; all other columns are optional.
401
+ */
402
+ generateCRUDUpdateJsonArg(entity) {
403
+ const fnName = this.getCRUDRoutineName(entity, CRUDType.Update);
404
+ const viewName = this.getBaseViewName(entity);
405
+ const permissions = this.generateCRUDPermissions(entity, fnName, CRUDType.Update);
406
+ const setClause = this.renderJsonArgUpdateSetClause(entity);
407
+ const trigger = this.generateTimestampTrigger(entity);
408
+ // PK lookup. Composite-PK entities aren't expected to hit JSON-arg in
409
+ // practice (no current wide entity has a composite PK), but the shape
410
+ // still needs to handle them — extract each PK from p_data and AND the
411
+ // WHERE clause.
412
+ 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');
413
+ const pkValidation = entity.PrimaryKeys.map((pk) => `p_data ? '${pk.Name}'`).join(' AND ');
414
+ const whereClause = entity.PrimaryKeys.map((pk) => `${pgDialect.QuoteIdentifier(pk.Name)} = v_${this.toSnakeCase(pk.CodeName)}`).join(' AND ');
415
+ return `
416
+ ------------------------------------------------------------
417
+ ----- UPDATE FUNCTION FOR ${entity.BaseTable} (JSON-arg shape)
418
+ ------------------------------------------------------------
419
+ ${this.generateDropAllOverloadsBlock(entity.SchemaName, fnName)}
420
+ CREATE OR REPLACE FUNCTION ${pgDialect.QuoteSchema(entity.SchemaName, fnName)}(p_data JSONB)
421
+ RETURNS SETOF ${pgDialect.QuoteSchema(entity.SchemaName, viewName)}
422
+ AS $$
423
+ DECLARE
424
+ ${pkExtractions}
425
+ v_updated_count INTEGER;
426
+ BEGIN
427
+ IF p_data IS NULL OR NOT (${pkValidation}) THEN
428
+ RAISE EXCEPTION '${fnName}: p_data must include ${entity.PrimaryKeys.map((pk) => `"${pk.Name}"`).join(', ')}';
429
+ END IF;
430
+
431
+ UPDATE ${pgDialect.QuoteSchema(entity.SchemaName, entity.BaseTable)}
432
+ SET
433
+ ${setClause}
434
+ WHERE
435
+ ${whereClause};
436
+
437
+ GET DIAGNOSTICS v_updated_count = ROW_COUNT;
438
+
439
+ IF v_updated_count = 0 THEN
440
+ -- Nothing was updated, return empty result set
441
+ RETURN;
442
+ END IF;
443
+
444
+ -- Return the updated record from the base view
445
+ RETURN QUERY
446
+ SELECT * FROM ${pgDialect.QuoteSchema(entity.SchemaName, viewName)}
447
+ WHERE ${whereClause};
448
+ END;
449
+ $$ LANGUAGE plpgsql;
450
+ ${permissions}
451
+
452
+ ${trigger}
453
+ `;
454
+ }
455
+ /**
456
+ * Generates the JSON-arg variant of `spCreate*` for wide entities. Single
457
+ * `p_data JSONB` parameter; uses `EXECUTE format(...)` to build the INSERT
458
+ * dynamically based on which keys are present in `p_data` so absent keys
459
+ * fall back to column DEFAULT (matching the typed-arg sproc's default-
460
+ * substitution behavior). Returns the newly created row from the base view.
461
+ *
462
+ * Single-UUID-PK entities auto-generate the PK if `p_data` doesn't include
463
+ * one (matching `gen_random_uuid()` default). Composite-PK entities require
464
+ * caller-supplied PKs.
465
+ */
466
+ generateCRUDCreateJsonArg(entity) {
467
+ const fnName = this.getCRUDRoutineName(entity, CRUDType.Create);
468
+ const viewName = this.getBaseViewName(entity);
469
+ const permissions = this.generateCRUDPermissions(entity, fnName, CRUDType.Create);
470
+ const firstKey = entity.FirstPrimaryKey;
471
+ const pkType = firstKey.Type.toLowerCase().trim();
472
+ const pkIsUuidSingle = (pkType === 'uniqueidentifier' || pkType === 'uuid') && entity.PrimaryKeys.length === 1;
473
+ const pkPgType = this.mapSQLType(firstKey.SQLFullType);
474
+ const pkColQuoted = pgDialect.QuoteIdentifier(firstKey.Name);
475
+ // Writable fields participating in the INSERT — exclude PK (handled
476
+ // separately so we can supply a generated UUID when key is absent),
477
+ // exclude virtual / special-date / non-API-writable.
478
+ const writableFields = entity.Fields.filter((f) => shouldIncludeFieldInParams(f, 'create') && !f.IsPrimaryKey);
479
+ // Per-field cast tokens for the dynamic INSERT. Built as a JS array so
480
+ // the sproc body iterates over (column-name, cast-expression) pairs at
481
+ // runtime to assemble the column / value lists from p_data keys.
482
+ //
483
+ // The cast expressions are emitted with `$1` (parameterized placeholder)
484
+ // instead of `p_data` because they end up inside an `EXECUTE v_sql USING
485
+ // p_data` call. PG's dynamic-SQL parser does NOT see the enclosing
486
+ // function's locals — `p_data` is unbound inside the dynamic SQL — so we
487
+ // bind it as `$1` and reference `($1->>'Field')` from inside the dynamic
488
+ // INSERT. This is the canonical safe pattern for dynamic SQL with values:
489
+ // values flow through the binding mechanism, never through string
490
+ // interpolation, so there's zero injection surface.
491
+ const fieldCastEntries = writableFields
492
+ .map((f) => {
493
+ const cast = this.renderJsonExtractAndCast(f).replace(/p_data/g, '$$1');
494
+ return ` WHEN '${f.Name}' THEN '${cast.replace(/'/g, "''")}'`;
495
+ })
496
+ .join('\n');
497
+ const fieldNamesArrayLiteral = writableFields.map((f) => `'${f.Name}'`).join(', ');
498
+ // ID resolution body: differs by PK strategy. Single-UUID PK is auto-
499
+ // generated when the caller doesn't supply one; everything else (composite,
500
+ // non-UUID) requires the caller to provide the key explicitly.
501
+ const idResolveBody = pkIsUuidSingle
502
+ ? ` IF p_data ? '${firstKey.Name}' THEN
503
+ v_id := (p_data->>'${firstKey.Name}')::${pkPgType};
504
+ ELSE
505
+ v_id := gen_random_uuid();
506
+ END IF;`
507
+ : ` IF NOT (p_data ? '${firstKey.Name}') THEN
508
+ RAISE EXCEPTION '${fnName}: p_data must include "${firstKey.Name}"';
509
+ END IF;
510
+ v_id := (p_data->>'${firstKey.Name}')::${pkPgType};`;
511
+ return `
512
+ ------------------------------------------------------------
513
+ ----- CREATE FUNCTION FOR ${entity.BaseTable} (JSON-arg shape)
514
+ ------------------------------------------------------------
515
+ ${this.generateDropAllOverloadsBlock(entity.SchemaName, fnName)}
516
+ CREATE OR REPLACE FUNCTION ${pgDialect.QuoteSchema(entity.SchemaName, fnName)}(p_data JSONB)
517
+ RETURNS SETOF ${pgDialect.QuoteSchema(entity.SchemaName, viewName)}
518
+ AS $$
519
+ DECLARE
520
+ v_id ${pkPgType};
521
+ v_field_name TEXT;
522
+ v_cast_expr TEXT;
523
+ v_col_list TEXT;
524
+ v_val_list TEXT;
525
+ v_sql TEXT;
526
+ BEGIN
527
+ ${idResolveBody}
528
+
529
+ v_col_list := quote_ident('${firstKey.Name.replace(/'/g, "''")}');
530
+ v_val_list := quote_literal(v_id) || '::${pkPgType}';
531
+
532
+ -- Build column / value lists from keys present in p_data. Absent keys are
533
+ -- omitted entirely so the column's DEFAULT applies (matching the typed-arg
534
+ -- sproc's default-substitution semantics).
535
+ FOREACH v_field_name IN ARRAY ARRAY[${fieldNamesArrayLiteral}]
536
+ LOOP
537
+ IF p_data ? v_field_name THEN
538
+ v_cast_expr := CASE v_field_name
539
+ ${fieldCastEntries}
540
+ END;
541
+ v_col_list := v_col_list || ', ' || quote_ident(v_field_name);
542
+ v_val_list := v_val_list || ', ' || v_cast_expr;
543
+ END IF;
544
+ END LOOP;
545
+
546
+ v_sql := format(
547
+ 'INSERT INTO ${pgDialect.QuoteSchema(entity.SchemaName, entity.BaseTable).replace(/'/g, "''")} (%s) VALUES (%s)',
548
+ v_col_list,
549
+ v_val_list
550
+ );
551
+ -- USING p_data binds the cast expressions' \`$1\` placeholders to the
552
+ -- function's p_data argument. Without USING, dynamic SQL has no access to
553
+ -- the enclosing function's locals and the \`$1->>\` references would fail
554
+ -- with \`column "p_data" does not exist\`.
555
+ EXECUTE v_sql USING p_data;
556
+
557
+ RETURN QUERY
558
+ SELECT * FROM ${pgDialect.QuoteSchema(entity.SchemaName, viewName)}
559
+ WHERE ${pkColQuoted} = v_id;
560
+ END;
561
+ $$ LANGUAGE plpgsql;
562
+ ${permissions}
128
563
  `;
129
564
  }
130
565
  /**
@@ -137,8 +572,16 @@ $do$;
137
572
  *
138
573
  * Prepends a DROP-all-overloads block (see `generateDropAllOverloadsBlock`) so
139
574
  * adding/removing a column doesn't trigger PG's overload-ambiguity error.
575
+ *
576
+ * Wide entities (where `useJsonArgShape` returns true) emit a JSON-arg variant
577
+ * via `generateCRUDCreateJsonArg` — single `p_data JSONB` parameter, dynamic
578
+ * INSERT built from keys present in the payload. Same semantics; different
579
+ * wire shape needed because of PostgreSQL's 100-arg function ceiling.
140
580
  */
141
581
  generateCRUDCreate(entity) {
582
+ if (this.shouldUseJsonArgShape(entity, 'create')) {
583
+ return this.generateCRUDCreateJsonArg(entity);
584
+ }
142
585
  const fnName = this.getCRUDRoutineName(entity, CRUDType.Create);
143
586
  const viewName = this.getBaseViewName(entity);
144
587
  const paramString = this.generateCRUDParamString(entity.Fields, false);
@@ -192,6 +635,9 @@ ${permissions}
192
635
  * and emits `GRANT EXECUTE` permissions.
193
636
  */
194
637
  generateCRUDUpdate(entity) {
638
+ if (this.shouldUseJsonArgShape(entity, 'update')) {
639
+ return this.generateCRUDUpdateJsonArg(entity);
640
+ }
195
641
  const fnName = this.getCRUDRoutineName(entity, CRUDType.Update);
196
642
  const viewName = this.getBaseViewName(entity);
197
643
  const paramString = this.generateCRUDParamString(entity.Fields, true);
@@ -416,7 +862,7 @@ WHERE ${ftsColName} IS NULL;
416
862
  const primaryKey = entity.FirstPrimaryKey.Name;
417
863
  const primaryKeyType = this.mapSQLType(entity.FirstPrimaryKey.SQLFullType);
418
864
  const fieldName = field.Name;
419
- const fnName = `fn_${this.toSnakeCase(entity.BaseTable)}_${this.toSnakeCase(fieldName)}_get_root_id`;
865
+ const fnName = this.getRootIDFunctionName(entity, field);
420
866
  return `
421
867
  ------------------------------------------------------------
422
868
  ----- ROOT ID FUNCTION FOR: ${entity.BaseTable}.${fieldName}
@@ -460,6 +906,22 @@ CREATE OR REPLACE FUNCTION ${pgDialect.QuoteSchema(entity.SchemaName, fnName)}(
460
906
  $$ LANGUAGE sql STABLE;
461
907
  `;
462
908
  }
909
+ /**
910
+ * Produces the canonical name for the recursive root-finder helper function
911
+ * generated for self-referencing fields. The function definition and the
912
+ * view's LATERAL-JOIN reference must agree on this name (caller side calls
913
+ * via `generateRootFieldJoin`, definition via `generateRootIDFunction`).
914
+ *
915
+ * Note: this intentionally does NOT match the baseline-shipped PascalCase
916
+ * `fn{Table}{Field}_GetRootID` form, because the baseline returns
917
+ * `TABLE("RootID" type)` and the view callers expect a scalar — using the
918
+ * baseline name would clash with `cannot change return type of existing
919
+ * function`. The snake_case scalar form is codegen's own naming space and
920
+ * is consistent with how downstream views are emitted.
921
+ */
922
+ getRootIDFunctionName(entity, field) {
923
+ return `fn_${this.toSnakeCase(entity.BaseTable)}_${this.toSnakeCase(field.Name)}_get_root_id`;
924
+ }
463
925
  /** @inheritdoc */
464
926
  generateRootFieldSelect(_entity, field, alias) {
465
927
  const rootFieldName = `Root${field.Name}`;
@@ -471,7 +933,7 @@ $$ LANGUAGE sql STABLE;
471
933
  * `OUTER APPLY`) to call scalar functions inline within a view definition.
472
934
  */
473
935
  generateRootFieldJoin(entity, field, alias) {
474
- const fnName = `fn_${this.toSnakeCase(entity.BaseTable)}_${this.toSnakeCase(field.Name)}_get_root_id`;
936
+ const fnName = this.getRootIDFunctionName(entity, field);
475
937
  const tableAlias = entity.BaseTableCodeName.charAt(0).toLowerCase();
476
938
  return `LEFT JOIN LATERAL (
477
939
  SELECT ${pgDialect.QuoteSchema(entity.SchemaName, fnName)}(${tableAlias}.${pgDialect.QuoteIdentifier(entity.FirstPrimaryKey.Name)}, ${tableAlias}.${pgDialect.QuoteIdentifier(field.Name)}) AS root_id
@@ -585,8 +1047,14 @@ END $$;
585
1047
  continue;
586
1048
  // _Clear companion is emitted immediately before its main parameter
587
1049
  // for nullable columns whose database default is non-NULL.
1050
+ //
1051
+ // Type was historically `bit DEFAULT 0` (a copy-paste from the
1052
+ // SQL Server emitter). PG has no implicit cast from integer to
1053
+ // its `bit` type, and `bit` here was meant to mean a boolean
1054
+ // anyway — so the function compiles but every caller that passes
1055
+ // `0`/`false` fails type-checking. Emit native PG `boolean DEFAULT FALSE`.
588
1056
  if (!ef.IsPrimaryKey && this.needsClearCompanion(ef)) {
589
- parts.push(`${dialect.ParameterRef(ef.CodeName + '_Clear')} bit${dialect.ParameterDefault('0')}`);
1057
+ parts.push(`${dialect.ParameterRef(ef.CodeName + '_Clear')} ${dialect.BooleanParameterType()}${dialect.ParameterDefault(dialect.BooleanLiteral(false))}`);
590
1058
  foundDefault = true;
591
1059
  }
592
1060
  const isOptional = !this.isParamRequired(ef, isUpdate);
@@ -615,14 +1083,18 @@ END $$;
615
1083
  // ─── ROUTINE NAMING ──────────────────────────────────────────────────
616
1084
  /** @inheritdoc */
617
1085
  getCRUDRoutineName(entity, type) {
618
- const snakeTable = this.toSnakeCase(entity.BaseTableCodeName);
1086
+ // Match the baseline-ported `sp{Verb}{TableCodeName}` convention (SQL Server
1087
+ // names ported verbatim into PG). The runtime PostgreSQLDataProvider calls
1088
+ // these names directly — diverging here (e.g. `fn_create_<snake>`) means new
1089
+ // CodeGen runs leave functions the runtime can never find.
1090
+ const tableCodeName = entity.BaseTableCodeName;
619
1091
  switch (type) {
620
1092
  case CRUDType.Create:
621
- return entity.spCreate || `fn_create_${snakeTable}`;
1093
+ return entity.spCreate || `spCreate${tableCodeName}`;
622
1094
  case CRUDType.Update:
623
- return entity.spUpdate || `fn_update_${snakeTable}`;
1095
+ return entity.spUpdate || `spUpdate${tableCodeName}`;
624
1096
  case CRUDType.Delete:
625
- return entity.spDelete || `fn_delete_${snakeTable}`;
1097
+ return entity.spDelete || `spDelete${tableCodeName}`;
626
1098
  }
627
1099
  }
628
1100
  // ─── SQL HEADERS ─────────────────────────────────────────────────────
@@ -645,11 +1117,34 @@ END $$;
645
1117
  }
646
1118
  // ─── UTILITY ─────────────────────────────────────────────────────────
647
1119
  /**
648
- * Maps a SQL default value expression to its PostgreSQL equivalent. Translates
649
- * SQL Server built-in functions (e.g., `NEWID()` to `gen_random_uuid()`,
650
- * `GETUTCDATE()` to `NOW() AT TIME ZONE 'UTC'`), strips outer parentheses and
651
- * surrounding single quotes, and re-applies quoting based on the {@link needsQuotes}
652
- * flag. Returns `'NULL'` for empty or whitespace-only input.
1120
+ * Renders a column DEFAULT expression for embedding in a generated SQL
1121
+ * statement. Handles three cases:
1122
+ *
1123
+ * 1. **SS-style function defaults** — `NEWID()` → `gen_random_uuid()`,
1124
+ * `GETUTCDATE()` → `NOW() AT TIME ZONE 'UTC'`, etc. Mapped via the
1125
+ * `functionMap` table.
1126
+ *
1127
+ * 2. **PG typed-literal defaults** — values like `'Pending'::character
1128
+ * varying`, `'-1'::integer`, `'F51358F3-...'::uuid`, `'{}'::jsonb`
1129
+ * that PG returns from `information_schema.columns.column_default`.
1130
+ * These are already fully-formed PG expressions; pass through
1131
+ * verbatim. PG handles them natively in every context this method's
1132
+ * output appears (INSERT VALUES, COALESCE inside INSERT/UPDATE,
1133
+ * CASE-WHEN inside the clear-companion pattern).
1134
+ *
1135
+ * The type name after `::` may be a single identifier OR multiple
1136
+ * words separated by spaces (`character varying`, `double precision`,
1137
+ * `time with time zone`, etc.). Without correctly matching multi-word
1138
+ * types, stripping the outer quotes and re-wrapping would produce
1139
+ * `'''Pending''::character varying'` — a string literal whose value
1140
+ * contains the cast syntax — and INSERTs would fail with "value too
1141
+ * long for type character varying(N)".
1142
+ *
1143
+ * 3. **Plain string defaults** — strip outer parens / quotes, then
1144
+ * optionally re-apply single-quote wrap (with `'` → `''` escaping)
1145
+ * based on the `needsQuotes` flag.
1146
+ *
1147
+ * Returns `'NULL'` for empty or whitespace-only input.
653
1148
  */
654
1149
  formatDefaultValue(defaultValue, needsQuotes) {
655
1150
  if (!defaultValue || defaultValue.trim().length === 0)
@@ -680,13 +1175,48 @@ END $$;
680
1175
  if (trimmedValue.startsWith('(') && trimmedValue.endsWith(')')) {
681
1176
  trimmedValue = trimmedValue.substring(1, trimmedValue.length - 1);
682
1177
  }
1178
+ // PG returns typed-literal defaults from
1179
+ // `information_schema.columns.column_default`, e.g.
1180
+ // `'Pending'::character varying` or `'-1'::integer` or
1181
+ // `'{}'::jsonb`. These are already fully-formed PG expressions and
1182
+ // pass through verbatim — PG handles them natively in every context
1183
+ // this method's output appears (INSERT VALUES, COALESCE inside
1184
+ // INSERT/UPDATE, CASE-WHEN inside the clear-companion pattern).
1185
+ // Empirically verified in PG 17 against `character varying`,
1186
+ // `integer`, `uuid`, `bigint`, `double precision`, `boolean`, and
1187
+ // `timestamp with time zone`.
1188
+ //
1189
+ // Stripping the leading-and-trailing quotes here is wrong: the value
1190
+ // re-wrap downstream would produce `'''Pending''::character
1191
+ // varying'` — a string literal whose content is the cast syntax —
1192
+ // and INSERTs would fail with "value too long for type character
1193
+ // varying(N)" because the literal is longer than the column.
1194
+ //
1195
+ // The type name after `::` may be a single identifier (`text`,
1196
+ // `uuid`, `integer`) OR multiple words separated by spaces
1197
+ // (`character varying`, `double precision`, `time with time zone`,
1198
+ // `timestamp without time zone`, `bit varying`), optionally followed
1199
+ // by a length parameter `(N)` and/or array suffix `[]`. The
1200
+ // `\w+(?:\s+\w+)*` portion captures both single-word and multi-word
1201
+ // type names. The original regex matched only single-identifier
1202
+ // types and silently fell through to the broken re-wrap path for
1203
+ // multi-word names — this fix extends the match without changing
1204
+ // behavior for the cases the original regex already caught.
1205
+ if (/^'.*'::\w+(?:\s+\w+)*(\s*\(\s*\d+\s*\))?(\s*\[\s*\])?$/.test(trimmedValue)) {
1206
+ return trimmedValue;
1207
+ }
683
1208
  // Remove surrounding quotes for clean value
684
1209
  let cleanValue = trimmedValue;
685
1210
  if (cleanValue.startsWith("'") && cleanValue.endsWith("'")) {
686
1211
  cleanValue = cleanValue.substring(1, cleanValue.length - 1);
687
1212
  }
688
- if (needsQuotes)
689
- return `'${cleanValue}'`;
1213
+ if (needsQuotes) {
1214
+ // Single-quote-escape the cleaned value (SQL standard: '' → ').
1215
+ // Without this, a default of `O'Brien` would re-emit as `'O'Brien'`
1216
+ // which closes the literal at the apostrophe and then fails parsing.
1217
+ const escaped = cleanValue.replace(/'/g, "''");
1218
+ return `'${escaped}'`;
1219
+ }
690
1220
  return cleanValue;
691
1221
  }
692
1222
  /**
@@ -752,7 +1282,15 @@ END $$;
752
1282
  addDefaultConstraintSQL(schema, tableName, columnName, defaultExpression) {
753
1283
  const table = pgDialect.QuoteSchema(schema, tableName);
754
1284
  const col = pgDialect.QuoteIdentifier(columnName);
755
- return `ALTER TABLE ${table} ALTER COLUMN ${col} SET DEFAULT ${defaultExpression}`;
1285
+ // Prepend SET CONSTRAINTS ALL IMMEDIATE so any deferred constraint
1286
+ // trigger events queued by earlier INSERTs in the same migration
1287
+ // transaction fire NOW, freeing the target table for ALTER. Without
1288
+ // this, mid-migration ALTER TABLE on a table that received earlier
1289
+ // INSERTs fails with `cannot ALTER TABLE because it has pending
1290
+ // trigger events` — codegen interleaves INSERTs (data sync) and
1291
+ // ALTER TABLE (default refresh) within a single Skyway-wrapped
1292
+ // transaction so the queue must be flushed at the boundary.
1293
+ return `SET CONSTRAINTS ALL IMMEDIATE;\nALTER TABLE ${table} ALTER COLUMN ${col} SET DEFAULT ${defaultExpression}`;
756
1294
  }
757
1295
  /**
758
1296
  * Generates a PL/pgSQL `DO $$` block that drops both a named CHECK constraint (if one
@@ -763,7 +1301,12 @@ END $$;
763
1301
  dropDefaultConstraintSQL(schema, tableName, columnName) {
764
1302
  const table = pgDialect.QuoteSchema(schema, tableName);
765
1303
  const col = pgDialect.QuoteIdentifier(columnName);
766
- return `
1304
+ // SET CONSTRAINTS ALL IMMEDIATE outside the DO block so it executes
1305
+ // first; any deferred trigger events from earlier INSERTs in the
1306
+ // same transaction fire NOW, clearing the queue before the ALTER
1307
+ // TABLE inside this DO block runs. See addDefaultConstraintSQL for
1308
+ // the rationale on why this is needed.
1309
+ return `SET CONSTRAINTS ALL IMMEDIATE;
767
1310
  DO $$
768
1311
  DECLARE
769
1312
  v_constraint_name TEXT;
@@ -807,7 +1350,7 @@ END $$`;
807
1350
  COALESCE(character_maximum_length, 0) AS "Length",
808
1351
  COALESCE(numeric_precision, 0) AS "Precision",
809
1352
  COALESCE(numeric_scale, 0) AS "Scale",
810
- CASE WHEN is_nullable = 'YES' THEN 1 ELSE 0 END AS "AllowsNull"
1353
+ CASE WHEN is_nullable = 'YES' THEN TRUE ELSE FALSE END AS "AllowsNull"
811
1354
  FROM information_schema.columns
812
1355
  WHERE table_schema = '${schema}'
813
1356
  AND table_name = '${viewName}'
@@ -890,6 +1433,11 @@ ORDER BY ordinal_position`;
890
1433
  // PL/pgSQL control flow
891
1434
  'NEW', 'OLD', 'FOUND', 'LOOP', 'WHILE', 'EXIT', 'CONTINUE',
892
1435
  'ELSIF', 'ELSEIF', 'STRICT',
1436
+ // Transaction / constraint control (used by SET CONSTRAINTS ALL IMMEDIATE
1437
+ // emitted before ALTER TABLE so deferred trigger events flush). Without
1438
+ // CONSTRAINTS / IMMEDIATE / DEFERRED in the keyword set, the tokenizer
1439
+ // double-quotes them as identifiers and PG rejects the resulting SQL.
1440
+ 'CONSTRAINTS', 'IMMEDIATE', 'DEFERRED', 'SAVEPOINT', 'RELEASE',
893
1441
  // SQL Server types
894
1442
  'NVARCHAR', 'VARCHAR', 'UNIQUEIDENTIFIER', 'DATETIMEOFFSET', 'DATETIME', 'DATETIME2',
895
1443
  'BIGINT', 'SMALLINT', 'TINYINT', 'FLOAT', 'REAL', 'DECIMAL', 'NUMERIC', 'MONEY',
@@ -1116,12 +1664,49 @@ WHERE p.prokind IN ('f', 'p')
1116
1664
  });
1117
1665
  await client.connect();
1118
1666
  try {
1667
+ // PG-only: emit recursive-FK root-ID helpers ahead of the view.
1668
+ //
1669
+ // PG codegen intentionally uses snake_case scalar
1670
+ // `fn_<table>_<field>_get_root_id` rather than the SS baseline's
1671
+ // PascalCase table-returning `fn<Table><Field>_GetRootID` — see
1672
+ // `getRootIDFunctionName`. The two names CAN'T coexist as the
1673
+ // same function (different return type), so codegen owns its
1674
+ // own snake_case namespace.
1675
+ //
1676
+ // Consequence: on a fresh PG database, the snake_case helpers
1677
+ // don't exist until codegen has run successfully at least once.
1678
+ // The `recompileAllBaseViews` → `regenerateFailedBaseViews`
1679
+ // recovery path used to call `generateBaseView` directly with
1680
+ // no helper emission, so customer first-run codegen against a
1681
+ // managed PG (where only baseline PascalCase functions exist)
1682
+ // hit `function fn_ai_agent_run_parent_run_id_get_root_id does
1683
+ // not exist` when LATERAL JOINs in the regenerated view body
1684
+ // resolved to the snake_case scalar form.
1685
+ //
1686
+ // SS doesn't have this problem because its codegen-generated
1687
+ // helper name matches the baseline name — the baseline always
1688
+ // satisfies the LATERAL JOIN. Hence the fix lives here, not
1689
+ // in the dialect-agnostic recovery loop.
1690
+ const recursiveFKs = entity.Fields.filter(f => f.RelatedEntityID != null && UUIDsEqual(f.RelatedEntityID, entity.ID));
1691
+ for (const field of recursiveFKs) {
1692
+ const fnSQL = this.generateRootIDFunction(entity, field);
1693
+ if (fnSQL && fnSQL.trim().length > 0) {
1694
+ await client.query(fnSQL);
1695
+ }
1696
+ }
1119
1697
  await executeWithFallback({
1120
1698
  client,
1121
1699
  schema: entity.SchemaName,
1122
1700
  viewName: entity.BaseView,
1123
1701
  createOrReplaceSQL: viewSQL,
1124
1702
  willRegenerate,
1703
+ // Pass the base table so viewFallback can materialize a stub
1704
+ // first if the view body has a self-reference and the view
1705
+ // doesn't yet exist (e.g. vwRecordChanges joins to itself for
1706
+ // parent lookup; if it was CASCADE-dropped earlier in the
1707
+ // same codegen run, CREATE OR REPLACE can't resolve the
1708
+ // self-reference until a placeholder exists).
1709
+ baseTableQualified: pgDialect.QuoteSchema(entity.SchemaName, entity.BaseTable),
1125
1710
  });
1126
1711
  }
1127
1712
  finally {
@@ -1189,6 +1774,7 @@ WHERE p.prokind IN ('f', 'p')
1189
1774
  viewName: opts.entity.BaseView,
1190
1775
  createOrReplaceSQL: opts.viewSQL,
1191
1776
  willRegenerate: opts.willRegenerate,
1777
+ baseTableQualified: pgDialect.QuoteSchema(opts.entity.SchemaName, opts.entity.BaseTable),
1192
1778
  });
1193
1779
  }
1194
1780
  catch (e) {
@@ -1680,12 +2266,12 @@ numbered_rows AS (
1680
2266
  e."Name" AS "EntityName",
1681
2267
  re."ID" AS "RelatedEntityID",
1682
2268
  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",
2269
+ CASE WHEN sf."FieldName" = 'Name' THEN TRUE ELSE FALSE END AS "IsNameField",
2270
+ CASE WHEN pk."ColumnName" IS NOT NULL THEN TRUE ELSE FALSE END AS "IsPrimaryKey",
1685
2271
  CASE
1686
- WHEN pk."ColumnName" IS NOT NULL THEN 1
1687
- WHEN uk."ColumnName" IS NOT NULL THEN 1
1688
- ELSE 0
2272
+ WHEN pk."ColumnName" IS NOT NULL THEN TRUE
2273
+ WHEN uk."ColumnName" IS NOT NULL THEN TRUE
2274
+ ELSE FALSE
1689
2275
  END AS "IsUnique",
1690
2276
  ROW_NUMBER() OVER (PARTITION BY sf."EntityID", sf."FieldName" ORDER BY (SELECT NULL)) AS rn
1691
2277
  FROM
@@ -1716,12 +2302,12 @@ ORDER BY "EntityID", "Sequence";
1716
2302
  * Builds the CASE expression for AllowUpdateAPI in the pending entity fields query.
1717
2303
  */
1718
2304
  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
2305
+ return `CASE WHEN sf."IsVirtual" = true THEN FALSE
2306
+ WHEN sf."FieldName" = '${EntityInfo.CreatedAtFieldName}' THEN FALSE
2307
+ WHEN sf."FieldName" = '${EntityInfo.UpdatedAtFieldName}' THEN FALSE
2308
+ WHEN sf."FieldName" = '${EntityInfo.DeletedAtFieldName}' THEN FALSE
2309
+ WHEN pk."ColumnName" IS NOT NULL THEN FALSE
2310
+ ELSE TRUE
1725
2311
  END AS "AllowUpdateAPI"`;
1726
2312
  }
1727
2313
  /**
@@ -1748,11 +2334,12 @@ WHERE vf."IsVirtual" = true
1748
2334
  AND fk."RelatedEntityNameFieldMap" != ''
1749
2335
  AND LOWER(vf."Name") = LOWER(fk."RelatedEntityNameFieldMap"))
1750
2336
  )
1751
- AND vf."AllowsNull" != fk."AllowsNull"`;
2337
+ AND vf."AllowsNull" != fk."AllowsNull";
2338
+ `;
1752
2339
  }
1753
2340
  };
1754
2341
  PostgreSQLCodeGenProvider = PostgreSQLCodeGenProvider_1 = __decorate([
1755
- RegisterClass(CodeGenDatabaseProvider, 'PostgreSQLCodeGenProvider')
2342
+ RegisterClass(CodeGenDatabaseProvider, 'postgresql')
1756
2343
  ], PostgreSQLCodeGenProvider);
1757
2344
  export { PostgreSQLCodeGenProvider };
1758
2345
  //# sourceMappingURL=PostgreSQLCodeGenProvider.js.map