@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.
- package/README.md +24 -0
- package/dist/Config/config.d.ts +19 -8
- package/dist/Config/config.d.ts.map +1 -1
- package/dist/Config/config.js +18 -8
- package/dist/Config/config.js.map +1 -1
- package/dist/Config/db-connection.d.ts.map +1 -1
- package/dist/Config/db-connection.js +8 -3
- package/dist/Config/db-connection.js.map +1 -1
- package/dist/Database/codeGenDatabaseProvider.d.ts +53 -0
- package/dist/Database/codeGenDatabaseProvider.d.ts.map +1 -1
- package/dist/Database/codeGenDatabaseProvider.js +66 -7
- package/dist/Database/codeGenDatabaseProvider.js.map +1 -1
- package/dist/Database/manage-metadata.d.ts +81 -4
- package/dist/Database/manage-metadata.d.ts.map +1 -1
- package/dist/Database/manage-metadata.js +245 -22
- package/dist/Database/manage-metadata.js.map +1 -1
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts +131 -20
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts.map +1 -1
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js +640 -53
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js.map +1 -1
- package/dist/Database/providers/postgresql/viewFallback.d.ts +14 -0
- package/dist/Database/providers/postgresql/viewFallback.d.ts.map +1 -1
- package/dist/Database/providers/postgresql/viewFallback.js +107 -13
- package/dist/Database/providers/postgresql/viewFallback.js.map +1 -1
- package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts +5 -2
- package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts.map +1 -1
- package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js +26 -7
- package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js.map +1 -1
- package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.test.js +40 -9
- package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.test.js.map +1 -1
- package/dist/Database/sql.d.ts +8 -0
- package/dist/Database/sql.d.ts.map +1 -1
- package/dist/Database/sql.js +18 -14
- package/dist/Database/sql.js.map +1 -1
- package/dist/Database/sql_codegen.d.ts +18 -5
- package/dist/Database/sql_codegen.d.ts.map +1 -1
- package/dist/Database/sql_codegen.js +83 -48
- package/dist/Database/sql_codegen.js.map +1 -1
- package/dist/Misc/action_subclasses_codegen.js +1 -1
- package/dist/Misc/action_subclasses_codegen.js.map +1 -1
- package/dist/Misc/createNewUser.d.ts.map +1 -1
- package/dist/Misc/createNewUser.js +14 -3
- package/dist/Misc/createNewUser.js.map +1 -1
- package/dist/Misc/graphql_server_codegen.d.ts +11 -0
- package/dist/Misc/graphql_server_codegen.d.ts.map +1 -1
- package/dist/Misc/graphql_server_codegen.js +27 -40
- package/dist/Misc/graphql_server_codegen.js.map +1 -1
- package/dist/Misc/sql_logging.d.ts.map +1 -1
- package/dist/Misc/sql_logging.js +16 -2
- package/dist/Misc/sql_logging.js.map +1 -1
- package/dist/Misc/system_integrity.js +3 -3
- package/dist/Misc/system_integrity.js.map +1 -1
- package/dist/Misc/temp_batch_file.js +2 -2
- package/dist/Misc/temp_batch_file.js.map +1 -1
- package/dist/runCodeGen.d.ts +1 -1
- package/dist/runCodeGen.d.ts.map +1 -1
- package/dist/runCodeGen.js +51 -6
- package/dist/runCodeGen.js.map +1 -1
- 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
|
|
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
|
-
* **
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
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
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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 =
|
|
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 =
|
|
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')}
|
|
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
|
-
|
|
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 || `
|
|
1093
|
+
return entity.spCreate || `spCreate${tableCodeName}`;
|
|
622
1094
|
case CRUDType.Update:
|
|
623
|
-
return entity.spUpdate || `
|
|
1095
|
+
return entity.spUpdate || `spUpdate${tableCodeName}`;
|
|
624
1096
|
case CRUDType.Delete:
|
|
625
|
-
return entity.spDelete || `
|
|
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
|
-
*
|
|
649
|
-
*
|
|
650
|
-
*
|
|
651
|
-
*
|
|
652
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
1684
|
-
CASE WHEN pk."ColumnName" IS NOT NULL THEN
|
|
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
|
|
1687
|
-
WHEN uk."ColumnName" IS NOT NULL THEN
|
|
1688
|
-
ELSE
|
|
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
|
|
1720
|
-
WHEN sf."FieldName" = '${EntityInfo.CreatedAtFieldName}' THEN
|
|
1721
|
-
WHEN sf."FieldName" = '${EntityInfo.UpdatedAtFieldName}' THEN
|
|
1722
|
-
WHEN sf."FieldName" = '${EntityInfo.DeletedAtFieldName}' THEN
|
|
1723
|
-
WHEN pk."ColumnName" IS NOT NULL THEN
|
|
1724
|
-
ELSE
|
|
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, '
|
|
2342
|
+
RegisterClass(CodeGenDatabaseProvider, 'postgresql')
|
|
1756
2343
|
], PostgreSQLCodeGenProvider);
|
|
1757
2344
|
export { PostgreSQLCodeGenProvider };
|
|
1758
2345
|
//# sourceMappingURL=PostgreSQLCodeGenProvider.js.map
|