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