@memberjunction/codegen-lib 5.41.0 → 5.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/Config/config.d.ts.map +1 -1
  2. package/dist/Config/config.js +7 -4
  3. package/dist/Config/config.js.map +1 -1
  4. package/dist/Config/db-connection.d.ts +38 -9
  5. package/dist/Config/db-connection.d.ts.map +1 -1
  6. package/dist/Config/db-connection.js +74 -37
  7. package/dist/Config/db-connection.js.map +1 -1
  8. package/dist/Database/codeGenDatabaseProvider.d.ts +16 -0
  9. package/dist/Database/codeGenDatabaseProvider.d.ts.map +1 -1
  10. package/dist/Database/codeGenDatabaseProvider.js +18 -0
  11. package/dist/Database/codeGenDatabaseProvider.js.map +1 -1
  12. package/dist/Database/manage-metadata.d.ts +26 -1
  13. package/dist/Database/manage-metadata.d.ts.map +1 -1
  14. package/dist/Database/manage-metadata.js +101 -2
  15. package/dist/Database/manage-metadata.js.map +1 -1
  16. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts +2 -0
  17. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts.map +1 -1
  18. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js +22 -2
  19. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js.map +1 -1
  20. package/dist/Database/providers/postgresql/metadataSupportObjects.d.ts +24 -0
  21. package/dist/Database/providers/postgresql/metadataSupportObjects.d.ts.map +1 -0
  22. package/dist/Database/providers/postgresql/metadataSupportObjects.js +650 -0
  23. package/dist/Database/providers/postgresql/metadataSupportObjects.js.map +1 -0
  24. package/dist/Database/sql_codegen.d.ts.map +1 -1
  25. package/dist/Database/sql_codegen.js +12 -8
  26. package/dist/Database/sql_codegen.js.map +1 -1
  27. package/dist/Misc/action_subclasses_codegen.d.ts.map +1 -1
  28. package/dist/Misc/action_subclasses_codegen.js +3 -1
  29. package/dist/Misc/action_subclasses_codegen.js.map +1 -1
  30. package/dist/Misc/runCommand.d.ts.map +1 -1
  31. package/dist/Misc/runCommand.js +8 -3
  32. package/dist/Misc/runCommand.js.map +1 -1
  33. package/dist/Misc/status_logging.d.ts +2 -0
  34. package/dist/Misc/status_logging.d.ts.map +1 -1
  35. package/dist/Misc/status_logging.js +12 -1
  36. package/dist/Misc/status_logging.js.map +1 -1
  37. package/dist/plugins/index.d.ts +35 -0
  38. package/dist/plugins/index.d.ts.map +1 -0
  39. package/dist/plugins/index.js +100 -0
  40. package/dist/plugins/index.js.map +1 -0
  41. package/dist/runCodeGen.d.ts +27 -0
  42. package/dist/runCodeGen.d.ts.map +1 -1
  43. package/dist/runCodeGen.js +104 -12
  44. package/dist/runCodeGen.js.map +1 -1
  45. package/package.json +32 -19
@@ -0,0 +1,650 @@
1
+ /**
2
+ * DDL for CodeGen's PostgreSQL metadata-management support objects.
3
+ *
4
+ * These objects (the vwSQLColumnsAndEntityFields introspection view, the
5
+ * default-value helpers, and the five metadata-management routines that
6
+ * manage-metadata invokes via callRoutineSQL) are CodeGen's OWN machinery —
7
+ * only CodeGen calls them. Shipping them through migrations created a
8
+ * sync-by-convention dependency that failed in practice: four of the five
9
+ * routines never existed on PG and CodeGen silently degraded (stale
10
+ * EntityField metadata — e.g. the vwApplicationSettings INNER JOIN bug that
11
+ * hid NULL-ApplicationID rows).
12
+ *
13
+ * Instead, CodeGen ensures these objects itself at the start of every
14
+ * manageMetadata run (DROP IF EXISTS + CREATE OR REPLACE — idempotent,
15
+ * cheap). They can never be missing or version-skewed: they always match
16
+ * the CodeGenLib version that calls them. SQL Server is unaffected (its
17
+ * counterparts ship in the baseline migrations); see
18
+ * CodeGenDatabaseProvider.getMetadataSupportObjectsSQL.
19
+ *
20
+ * The DDL is authored as plain PostgreSQL against the literal __mj schema
21
+ * and parameterized at runtime; String.raw keeps regex backslashes intact.
22
+ */
23
+ export function buildMetadataSupportObjectsSQL(mjCoreSchema) {
24
+ return METADATA_SUPPORT_OBJECTS_DDL.split('__mj').join(mjCoreSchema);
25
+ }
26
+ const METADATA_SUPPORT_OBJECTS_DDL = String.raw `
27
+ -- ----------------------------------------------------------------------------
28
+ -- 0. Drop the view first — it depends on fnMapPGDefaultToMJ, so on re-apply
29
+ -- the helper DROPs below would otherwise fail with a dependency error.
30
+ -- ----------------------------------------------------------------------------
31
+ DROP VIEW IF EXISTS __mj."vwSQLColumnsAndEntityFields";
32
+
33
+ -- ----------------------------------------------------------------------------
34
+ -- 1. Default-value helpers
35
+ -- ----------------------------------------------------------------------------
36
+
37
+ -- Normalizer used ONLY for change detection. Collapses formatting variance
38
+ -- (paren wrapping, N-prefix, ::casts, current-timestamp function family, uuid
39
+ -- generator family, numeric trailing zeros) so cosmetic differences never
40
+ -- trigger metadata updates. Both sides of every comparison pass through this,
41
+ -- so lossy transforms are safe.
42
+ DROP FUNCTION IF EXISTS __mj."fnNormalizeDefaultValue"(TEXT);
43
+ CREATE OR REPLACE FUNCTION __mj."fnNormalizeDefaultValue"(v TEXT)
44
+ RETURNS TEXT
45
+ LANGUAGE sql IMMUTABLE AS $$
46
+ SELECT CASE WHEN v IS NULL THEN NULL ELSE
47
+ regexp_replace( -- 1.0 / 0.000000 -> 1 / 0
48
+ translate( -- strip parens and quotes
49
+ regexp_replace( -- user-name family -> user
50
+ regexp_replace( -- timestamp family -> now
51
+ regexp_replace( -- uuid family -> newid
52
+ regexp_replace( -- N'x' -> 'x'
53
+ regexp_replace(lower(trim(v)), -- strip ::casts
54
+ '\s*::[a-z_ ]+(\([0-9, ]*\))?', '', 'g'),
55
+ 'n''', '''', 'g'),
56
+ '(gen_random_uuid|uuid_generate_v4|newsequentialid|newid)\(\)', 'newid', 'g'),
57
+ '(now\(\)\s+at\s+time\s+zone\s+''utc''|getutcdate\(\)|getdate\(\)|sysdatetimeoffset\(\)|sysutcdatetime\(\)|current_timestamp|now\(\))', 'now', 'g'),
58
+ '(user_name\(\)|suser_name\(\)|suser_sname\(\)|current_user|session_user)', 'user', 'g'),
59
+ '()''', ''),
60
+ '^\s*(-?[0-9]+)\.0+\s*$', '\1')
61
+ END
62
+ $$;
63
+
64
+ -- Maps a pg_get_expr() default expression to MJ-canonical (SS-flavored) text
65
+ -- for storage/emission. Unmappable expressions pass through unchanged — the
66
+ -- normalizer above keeps them from churning metadata.
67
+ DROP FUNCTION IF EXISTS __mj."fnMapPGDefaultToMJ"(TEXT);
68
+ CREATE OR REPLACE FUNCTION __mj."fnMapPGDefaultToMJ"(v TEXT)
69
+ RETURNS TEXT
70
+ LANGUAGE sql IMMUTABLE AS $$
71
+ SELECT CASE
72
+ WHEN v IS NULL THEN NULL
73
+ WHEN v ~* 'now\(\)\s+at\s+time\s+zone\s+''utc''' THEN '(getutcdate())'
74
+ WHEN lower(trim(v)) IN ('now()', 'current_timestamp', 'transaction_timestamp()', 'statement_timestamp()') THEN '(sysdatetimeoffset())'
75
+ WHEN lower(trim(v)) IN ('gen_random_uuid()', 'uuid_generate_v4()') THEN '(newsequentialid())'
76
+ WHEN lower(trim(v)) = 'true' THEN '((1))'
77
+ WHEN lower(trim(v)) = 'false' THEN '((0))'
78
+ WHEN trim(v) ~ '^-?[0-9]+(\.[0-9]+)?$' THEN '((' || trim(v) || '))'
79
+ WHEN trim(v) ~* '^''[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}''::uuid$'
80
+ THEN '(''' || upper(substring(trim(v) from '^''(.*)''::')) || ''')'
81
+ WHEN trim(v) ~ '^''.*''::[a-z_ ]+(\([0-9, ]*\))?$'
82
+ THEN '(N''' || substring(trim(v) from '^''(.*)''::') || ''')'
83
+ ELSE v
84
+ END
85
+ $$;
86
+
87
+ -- ----------------------------------------------------------------------------
88
+ -- 2. Replace vwSQLColumnsAndEntityFields (column set changes: +IsComputed,
89
+ -- Type becomes text) — no dependent views exist on this view.
90
+ -- (dropped in section 0)
91
+ -- ----------------------------------------------------------------------------
92
+ CREATE VIEW __mj."vwSQLColumnsAndEntityFields" AS
93
+ SELECT
94
+ e."EntityID",
95
+ e."EntityName" AS "Entity",
96
+ e."SchemaName",
97
+ e."TableName",
98
+ ef."ID" AS "EntityFieldID",
99
+ ef."Sequence" AS "EntityFieldSequence",
100
+ ef."Name" AS "EntityFieldName",
101
+ a.attnum AS "Sequence",
102
+ bt_a.attnum AS "BaseTableSequence",
103
+ a.attname AS "FieldName",
104
+ -- MJ-canonical type vocabulary (matches EntityField.Type, which is shared
105
+ -- with SQL Server installs). Unknown PG types pass through.
106
+ CASE COALESCE(base_t.typname, t.typname)
107
+ WHEN 'varchar' THEN 'nvarchar'
108
+ WHEN 'text' THEN 'nvarchar'
109
+ WHEN 'bpchar' THEN 'nchar'
110
+ WHEN 'char' THEN 'nchar'
111
+ WHEN 'uuid' THEN 'uniqueidentifier'
112
+ WHEN 'bool' THEN 'bit'
113
+ WHEN 'timestamptz' THEN 'datetimeoffset'
114
+ WHEN 'timestamp' THEN 'datetime'
115
+ WHEN 'int4' THEN 'int'
116
+ WHEN 'int2' THEN 'smallint'
117
+ WHEN 'int8' THEN 'bigint'
118
+ WHEN 'numeric' THEN 'decimal'
119
+ WHEN 'float8' THEN 'float'
120
+ WHEN 'float4' THEN 'real'
121
+ WHEN 'bytea' THEN 'varbinary'
122
+ WHEN 'jsonb' THEN 'nvarchar'
123
+ WHEN 'json' THEN 'nvarchar'
124
+ ELSE COALESCE(base_t.typname, t.typname)::text
125
+ END AS "Type",
126
+ CASE WHEN t.typtype = 'd' THEN t.typname::text ELSE NULL END AS "UserDefinedType",
127
+ -- SS sys.columns max_length semantics: bytes (2x chars for n(var)char),
128
+ -- -1 for MAX/unbounded, decimal storage bytes by precision band.
129
+ CASE
130
+ WHEN COALESCE(base_t.typname, t.typname) IN ('varchar', 'bpchar', 'char') THEN
131
+ CASE WHEN a.atttypmod = -1 THEN -1 ELSE (a.atttypmod - 4) * 2 END
132
+ WHEN COALESCE(base_t.typname, t.typname) IN ('text', 'bytea', 'jsonb', 'json') THEN -1
133
+ WHEN COALESCE(base_t.typname, t.typname) = 'uuid' THEN 16
134
+ WHEN COALESCE(base_t.typname, t.typname) = 'bool' THEN 1
135
+ WHEN COALESCE(base_t.typname, t.typname) = 'timestamptz' THEN 10
136
+ WHEN COALESCE(base_t.typname, t.typname) = 'timestamp' THEN 8
137
+ WHEN COALESCE(base_t.typname, t.typname) = 'numeric' THEN
138
+ CASE
139
+ WHEN a.atttypmod = -1 THEN 17
140
+ WHEN (((a.atttypmod - 4) >> 16) & 65535) <= 9 THEN 5
141
+ WHEN (((a.atttypmod - 4) >> 16) & 65535) <= 19 THEN 9
142
+ WHEN (((a.atttypmod - 4) >> 16) & 65535) <= 28 THEN 13
143
+ ELSE 17
144
+ END
145
+ ELSE a.attlen::integer
146
+ END AS "Length",
147
+ CASE
148
+ WHEN COALESCE(base_t.typname, t.typname) = 'numeric' AND a.atttypmod <> -1 THEN ((a.atttypmod - 4) >> 16) & 65535
149
+ WHEN COALESCE(base_t.typname, t.typname) = 'numeric' THEN 38
150
+ WHEN COALESCE(base_t.typname, t.typname) = 'int4' THEN 10
151
+ WHEN COALESCE(base_t.typname, t.typname) = 'int2' THEN 5
152
+ WHEN COALESCE(base_t.typname, t.typname) = 'int8' THEN 19
153
+ WHEN COALESCE(base_t.typname, t.typname) = 'timestamptz' THEN 34
154
+ WHEN COALESCE(base_t.typname, t.typname) = 'timestamp' THEN 23
155
+ WHEN COALESCE(base_t.typname, t.typname) = 'bool' THEN 1
156
+ WHEN COALESCE(base_t.typname, t.typname) = 'float8' THEN 53
157
+ WHEN COALESCE(base_t.typname, t.typname) = 'float4' THEN 24
158
+ ELSE 0
159
+ END AS "Precision",
160
+ CASE
161
+ WHEN COALESCE(base_t.typname, t.typname) = 'numeric' AND a.atttypmod <> -1 THEN (a.atttypmod - 4) & 65535
162
+ WHEN COALESCE(base_t.typname, t.typname) = 'timestamptz' THEN 7
163
+ WHEN COALESCE(base_t.typname, t.typname) = 'timestamp' THEN 3
164
+ ELSE 0
165
+ END AS "Scale",
166
+ -- Nullability must come from the BASE TABLE attribute when one exists: PG
167
+ -- view columns never carry NOT NULL, which is exactly the bug the previous
168
+ -- definition had. Virtual (view-only) fields fall back to the view attr.
169
+ CASE WHEN bt_a.attnum IS NOT NULL THEN NOT bt_a.attnotnull ELSE NOT a.attnotnull END AS "AllowsNull",
170
+ CASE WHEN COALESCE(bt_a.attidentity, '') IN ('a', 'd') THEN 1 ELSE 0 END AS "AutoIncrement",
171
+ a.attnum AS column_id,
172
+ CASE WHEN bt_a.attnum IS NULL THEN 1 ELSE 0 END AS "IsVirtual",
173
+ CASE WHEN COALESCE(bt_a.attgenerated, '') <> '' THEN 1 ELSE 0 END AS "IsComputed",
174
+ src_cls.oid AS object_id,
175
+ NULL::text AS "DefaultConstraintName",
176
+ __mj."fnMapPGDefaultToMJ"(pg_get_expr(ad.adbin, ad.adrelid)) AS "DefaultValue",
177
+ CASE WHEN COALESCE(bt_a.attgenerated, '') <> '' THEN pg_get_expr(ad.adbin, ad.adrelid) ELSE NULL END AS "ComputedColumnDefinition",
178
+ COALESCE(col_description(src_cls.oid, a.attnum::integer), col_description(bt_cls.oid, bt_a.attnum::integer)) AS "Description",
179
+ col_description(src_cls.oid, a.attnum::integer) AS "ViewColumnDescription",
180
+ CASE WHEN bt_a.attnum IS NOT NULL THEN col_description(bt_cls.oid, bt_a.attnum::integer) ELSE NULL END AS "TableColumnDescription"
181
+ FROM __mj."vwSQLTablesAndEntities" e
182
+ JOIN pg_class src_cls ON src_cls.oid = COALESCE(e.view_object_id, e.object_id)
183
+ JOIN pg_attribute a ON a.attrelid = src_cls.oid AND a.attnum > 0 AND NOT a.attisdropped
184
+ JOIN pg_type t ON a.atttypid = t.oid
185
+ LEFT JOIN pg_type base_t ON t.typbasetype = base_t.oid AND t.typtype = 'd'
186
+ JOIN pg_class bt_cls ON bt_cls.oid = e.object_id
187
+ LEFT JOIN pg_attribute bt_a ON bt_a.attrelid = bt_cls.oid AND bt_a.attname = a.attname AND bt_a.attnum > 0 AND NOT bt_a.attisdropped
188
+ LEFT JOIN pg_attrdef ad ON ad.adrelid = bt_cls.oid AND ad.adnum = bt_a.attnum
189
+ LEFT JOIN __mj."EntityField" ef ON e."EntityID" = ef."EntityID" AND a.attname = ef."Name"::text;
190
+
191
+ -- ----------------------------------------------------------------------------
192
+ -- 3. spUpdateExistingEntityFieldsFromSchema
193
+ -- (port of the SQL Server proc of the same name; consumed by CodeGen's
194
+ -- manage-metadata step 3 which reads "EntityName" from the result rows)
195
+ -- ----------------------------------------------------------------------------
196
+ DROP FUNCTION IF EXISTS __mj."spUpdateExistingEntityFieldsFromSchema"(TEXT);
197
+ DROP FUNCTION IF EXISTS __mj."spUpdateExistingEntityFieldsFromSchema"(TEXT, TEXT);
198
+ CREATE OR REPLACE FUNCTION __mj."spUpdateExistingEntityFieldsFromSchema"(
199
+ p_ExcludedSchemaNames TEXT,
200
+ p_EntityIDs TEXT DEFAULT NULL
201
+ )
202
+ RETURNS TABLE(
203
+ "EntityID" UUID,
204
+ "EntityName" TEXT,
205
+ "EntityFieldID" UUID,
206
+ "EntityFieldName" TEXT,
207
+ "AutoUpdateDescription" BOOLEAN,
208
+ "ExistingDescription" TEXT,
209
+ "SQLDescription" TEXT,
210
+ "Type" TEXT,
211
+ "Length" INTEGER,
212
+ "Precision" INTEGER,
213
+ "Scale" INTEGER,
214
+ "AllowsNull" BOOLEAN,
215
+ "DefaultValue" TEXT,
216
+ "AutoIncrement" BOOLEAN,
217
+ "IsVirtual" BOOLEAN,
218
+ "IsComputed" BOOLEAN,
219
+ "Sequence" INTEGER,
220
+ "RelatedEntityID" UUID,
221
+ "RelatedEntityFieldName" TEXT,
222
+ "IsPrimaryKey" BOOLEAN,
223
+ "IsUnique" BOOLEAN
224
+ )
225
+ LANGUAGE plpgsql AS $func$
226
+ DECLARE
227
+ v_is_scoped BOOLEAN := FALSE;
228
+ BEGIN
229
+ DROP TABLE IF EXISTS _uef_excluded;
230
+ CREATE TEMP TABLE _uef_excluded AS
231
+ SELECT TRIM(s) AS schema_name
232
+ FROM unnest(string_to_array(COALESCE(p_ExcludedSchemaNames, ''), ',')) AS s
233
+ WHERE TRIM(s) <> '';
234
+
235
+ DROP TABLE IF EXISTS _uef_scope;
236
+ CREATE TEMP TABLE _uef_scope AS
237
+ SELECT DISTINCT TRIM(v)::uuid AS entity_id
238
+ FROM unnest(string_to_array(COALESCE(p_EntityIDs, ''), ',')) AS v
239
+ WHERE TRIM(v) ~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$';
240
+ v_is_scoped := EXISTS (SELECT 1 FROM _uef_scope);
241
+
242
+ DROP TABLE IF EXISTS _uef_filtered;
243
+ CREATE TEMP TABLE _uef_filtered AS
244
+ SELECT
245
+ e."ID" AS entity_id,
246
+ e."Name" AS entity_name,
247
+ ef."ID" AS entity_field_id,
248
+ ef."Name" AS entity_field_name,
249
+ ef."AutoUpdateDescription" AS auto_update_description,
250
+ ef."Description" AS existing_description,
251
+ sq."Description" AS sql_description,
252
+ -- numeric/decimal are SS synonyms — keep whichever the metadata already has
253
+ CASE WHEN ef."Type" = 'numeric' AND sq."Type" = 'decimal' THEN ef."Type" ELSE sq."Type" END AS new_type,
254
+ sq."Length" AS new_length,
255
+ sq."Precision" AS new_precision,
256
+ sq."Scale" AS new_scale,
257
+ sq."AllowsNull" AS new_allows_null,
258
+ -- only rewrite a stored default when it differs SEMANTICALLY
259
+ CASE WHEN __mj."fnNormalizeDefaultValue"(ef."DefaultValue") IS NOT DISTINCT FROM __mj."fnNormalizeDefaultValue"(sq."DefaultValue")
260
+ THEN ef."DefaultValue" ELSE sq."DefaultValue" END AS new_default_value,
261
+ (sq."AutoIncrement" <> 0) AS new_auto_increment,
262
+ (sq."IsVirtual" <> 0) AS new_is_virtual,
263
+ (sq."IsComputed" <> 0) AS new_is_computed,
264
+ sq."Sequence" AS new_sequence,
265
+ re."ID" AS related_entity_id,
266
+ fk."referenced_column"::text AS related_entity_field_name,
267
+ (pk."ColumnName" IS NOT NULL) AS new_is_primary_key,
268
+ (pk."ColumnName" IS NOT NULL OR uk."ColumnName" IS NOT NULL) AS new_is_unique
269
+ FROM __mj."EntityField" ef
270
+ INNER JOIN __mj."vwSQLColumnsAndEntityFields" sq
271
+ ON ef."EntityID" = sq."EntityID" AND ef."Name"::text = sq."FieldName"::text
272
+ INNER JOIN __mj."Entity" e ON ef."EntityID" = e."ID"
273
+ LEFT JOIN __mj."vwForeignKeys" fk
274
+ ON ef."Name"::text = fk."column"::text AND e."BaseTable"::text = fk."table"::text AND e."SchemaName"::text = fk."schema_name"::text
275
+ LEFT JOIN __mj."Entity" re
276
+ ON re."BaseTable"::text = fk."referenced_table"::text AND re."SchemaName"::text = fk."referenced_schema"::text
277
+ LEFT JOIN __mj."vwTablePrimaryKeys" pk
278
+ ON e."BaseTable"::text = pk."TableName"::text AND ef."Name"::text = pk."ColumnName"::text AND e."SchemaName"::text = pk."SchemaName"::text
279
+ LEFT JOIN __mj."vwTableUniqueKeys" uk
280
+ ON e."BaseTable"::text = uk."TableName"::text AND ef."Name"::text = uk."ColumnName"::text AND e."SchemaName"::text = uk."SchemaName"::text
281
+ LEFT JOIN _uef_excluded ex ON e."SchemaName"::text = ex.schema_name
282
+ WHERE e."VirtualEntity" = FALSE
283
+ AND ex.schema_name IS NULL
284
+ AND (NOT v_is_scoped OR e."ID" IN (SELECT s.entity_id FROM _uef_scope s))
285
+ AND (
286
+ COALESCE(TRIM(ef."Description"), '') <>
287
+ COALESCE(TRIM(CASE WHEN ef."AutoUpdateDescription" THEN sq."Description" ELSE ef."Description" END), '')
288
+ -- Physical attributes are only compared for REAL columns. PG cannot
289
+ -- derive type facts for view-only (virtual) columns the way SQL Server
290
+ -- can (view columns are unbounded/nullable in pg_attribute), so virtual
291
+ -- fields keep their metadata values; the CodeGen TS pipeline's
292
+ -- getFixVirtualFieldNullabilitySQL step owns virtual-field nullability.
293
+ OR (sq."IsVirtual" = 0 AND (
294
+ (ef."Type" <> sq."Type" AND NOT (ef."Type" = 'numeric' AND sq."Type" = 'decimal'))
295
+ OR ef."Length" <> sq."Length"
296
+ OR ef."Precision" <> sq."Precision"
297
+ OR ef."Scale" <> sq."Scale"
298
+ OR ef."AllowsNull" <> sq."AllowsNull"
299
+ ))
300
+ OR __mj."fnNormalizeDefaultValue"(ef."DefaultValue") IS DISTINCT FROM __mj."fnNormalizeDefaultValue"(sq."DefaultValue")
301
+ OR ef."AutoIncrement" <> (sq."AutoIncrement" <> 0)
302
+ OR ef."IsVirtual" <> (sq."IsVirtual" <> 0)
303
+ OR ef."IsComputed" <> (sq."IsComputed" <> 0)
304
+ OR ef."Sequence" <> sq."Sequence"
305
+ OR COALESCE(ef."RelatedEntityID", '00000000-0000-0000-0000-000000000000'::uuid) <>
306
+ COALESCE(re."ID", '00000000-0000-0000-0000-000000000000'::uuid)
307
+ OR COALESCE(TRIM(ef."RelatedEntityFieldName"), '') <> COALESCE(TRIM(fk."referenced_column"::text), '')
308
+ OR ef."IsPrimaryKey" <> (pk."ColumnName" IS NOT NULL)
309
+ OR ef."IsUnique" <> (pk."ColumnName" IS NOT NULL OR uk."ColumnName" IS NOT NULL)
310
+ -- AllowUpdateAPI needs clearing on a real->virtual transition
311
+ OR (ef."AllowUpdateAPI" = TRUE AND sq."IsVirtual" <> 0 AND ef."IsVirtual" = FALSE)
312
+ );
313
+
314
+ -- PG checks UNIQUE row-by-row (SQL Server checks at statement end), so an
315
+ -- in-place renumber can transiently collide on UQ_EntityField_EntityID_Sequence.
316
+ -- Park changing sequences in a collision-free zone first; the real values are
317
+ -- assigned by the main UPDATE below.
318
+ UPDATE __mj."EntityField" tgt SET
319
+ "Sequence" = tgt."Sequence" + 1000000
320
+ FROM _uef_filtered fr
321
+ WHERE tgt."ID" = fr.entity_field_id
322
+ AND tgt."Sequence" IS DISTINCT FROM fr.new_sequence;
323
+
324
+ UPDATE __mj."EntityField" tgt SET
325
+ "Description" = CASE WHEN fr.auto_update_description THEN fr.sql_description ELSE tgt."Description" END,
326
+ -- physical attributes frozen for virtual fields (see WHERE clause comment)
327
+ "Type" = CASE WHEN fr.new_is_virtual THEN tgt."Type" ELSE fr.new_type END,
328
+ "Length" = CASE WHEN fr.new_is_virtual THEN tgt."Length" ELSE fr.new_length END,
329
+ "Precision" = CASE WHEN fr.new_is_virtual THEN tgt."Precision" ELSE fr.new_precision END,
330
+ "Scale" = CASE WHEN fr.new_is_virtual THEN tgt."Scale" ELSE fr.new_scale END,
331
+ "AllowsNull" = CASE WHEN fr.new_is_virtual THEN tgt."AllowsNull" ELSE fr.new_allows_null END,
332
+ "DefaultValue" = fr.new_default_value,
333
+ "AutoIncrement" = fr.new_auto_increment,
334
+ "IsVirtual" = fr.new_is_virtual,
335
+ "IsComputed" = fr.new_is_computed,
336
+ "Sequence" = fr.new_sequence,
337
+ "RelatedEntityID" = CASE WHEN tgt."AutoUpdateRelatedEntityInfo" THEN fr.related_entity_id ELSE tgt."RelatedEntityID" END,
338
+ "RelatedEntityFieldName" = CASE WHEN tgt."AutoUpdateRelatedEntityInfo" THEN fr.related_entity_field_name ELSE tgt."RelatedEntityFieldName" END,
339
+ "IsPrimaryKey" = fr.new_is_primary_key,
340
+ "IsUnique" = fr.new_is_unique,
341
+ -- when a field transitions to virtual it can no longer be written to
342
+ "AllowUpdateAPI" = CASE WHEN fr.new_is_virtual AND NOT tgt."IsVirtual" THEN FALSE ELSE tgt."AllowUpdateAPI" END,
343
+ "__mj_UpdatedAt" = now()
344
+ FROM _uef_filtered fr
345
+ WHERE tgt."ID" = fr.entity_field_id;
346
+
347
+ RETURN QUERY
348
+ SELECT
349
+ fr.entity_id, fr.entity_name::text, fr.entity_field_id, fr.entity_field_name::text,
350
+ fr.auto_update_description, fr.existing_description::text, fr.sql_description::text,
351
+ fr.new_type::text, fr.new_length::integer, fr.new_precision::integer, fr.new_scale::integer,
352
+ fr.new_allows_null, fr.new_default_value::text, fr.new_auto_increment,
353
+ fr.new_is_virtual, fr.new_is_computed, fr.new_sequence::integer,
354
+ fr.related_entity_id, fr.related_entity_field_name::text,
355
+ fr.new_is_primary_key, fr.new_is_unique
356
+ FROM _uef_filtered fr;
357
+
358
+ DROP TABLE IF EXISTS _uef_filtered;
359
+ DROP TABLE IF EXISTS _uef_scope;
360
+ DROP TABLE IF EXISTS _uef_excluded;
361
+ END;
362
+ $func$;
363
+
364
+ -- ----------------------------------------------------------------------------
365
+ -- 4. spUpdateExistingEntitiesFromSchema
366
+ -- (consumer reads "Name" from result rows)
367
+ -- ----------------------------------------------------------------------------
368
+ DROP FUNCTION IF EXISTS __mj."spUpdateExistingEntitiesFromSchema"(TEXT);
369
+ CREATE OR REPLACE FUNCTION __mj."spUpdateExistingEntitiesFromSchema"(p_ExcludedSchemaNames TEXT)
370
+ RETURNS TABLE(
371
+ "ID" UUID,
372
+ "Name" TEXT,
373
+ "CurrentDescription" TEXT,
374
+ "NewDescription" TEXT,
375
+ "EntityDescription" TEXT,
376
+ "SchemaName" TEXT
377
+ )
378
+ LANGUAGE plpgsql AS $func$
379
+ BEGIN
380
+ DROP TABLE IF EXISTS _ues_filtered;
381
+ CREATE TEMP TABLE _ues_filtered AS
382
+ SELECT
383
+ e."ID" AS entity_id,
384
+ e."Name" AS entity_name,
385
+ e."Description" AS current_description,
386
+ CASE WHEN e."AutoUpdateDescription" THEN sq."EntityDescription" ELSE e."Description" END AS new_description,
387
+ sq."EntityDescription" AS entity_description,
388
+ sq."SchemaName"::text AS schema_name
389
+ FROM __mj."Entity" e
390
+ INNER JOIN __mj."vwSQLTablesAndEntities" sq ON e."ID" = sq."EntityID"
391
+ LEFT JOIN unnest(string_to_array(COALESCE(p_ExcludedSchemaNames, ''), ',')) AS ex(v)
392
+ ON sq."SchemaName"::text = TRIM(ex.v)
393
+ WHERE e."VirtualEntity" = FALSE
394
+ AND ex.v IS NULL
395
+ AND COALESCE(CASE WHEN e."AutoUpdateDescription" THEN sq."EntityDescription" ELSE e."Description" END, '')
396
+ <> COALESCE(e."Description", '');
397
+
398
+ UPDATE __mj."Entity" tgt SET
399
+ "Description" = fr.new_description,
400
+ "__mj_UpdatedAt" = now()
401
+ FROM _ues_filtered fr
402
+ WHERE tgt."ID" = fr.entity_id;
403
+
404
+ RETURN QUERY
405
+ SELECT fr.entity_id, fr.entity_name::text, fr.current_description::text,
406
+ fr.new_description::text, fr.entity_description::text, fr.schema_name
407
+ FROM _ues_filtered fr;
408
+
409
+ DROP TABLE IF EXISTS _ues_filtered;
410
+ END;
411
+ $func$;
412
+
413
+ -- ----------------------------------------------------------------------------
414
+ -- 5. spDeleteUnneededEntityFields
415
+ -- Removes EntityField rows whose underlying column no longer exists.
416
+ -- (consumer reads "Entity" — the entity name — from result rows; the SS
417
+ -- version returns the full vwEntityFields row, but only "Entity" is used)
418
+ -- ----------------------------------------------------------------------------
419
+ DROP FUNCTION IF EXISTS __mj."spDeleteUnneededEntityFields"(TEXT);
420
+ DROP FUNCTION IF EXISTS __mj."spDeleteUnneededEntityFields"(TEXT, TEXT);
421
+ CREATE OR REPLACE FUNCTION __mj."spDeleteUnneededEntityFields"(
422
+ p_ExcludedSchemaNames TEXT,
423
+ p_EntityIDs TEXT DEFAULT NULL
424
+ )
425
+ RETURNS TABLE(
426
+ "ID" UUID,
427
+ "EntityID" UUID,
428
+ "Entity" TEXT,
429
+ "Name" TEXT
430
+ )
431
+ LANGUAGE plpgsql AS $func$
432
+ DECLARE
433
+ v_is_scoped BOOLEAN := FALSE;
434
+ BEGIN
435
+ DROP TABLE IF EXISTS _del_scope;
436
+ CREATE TEMP TABLE _del_scope AS
437
+ SELECT DISTINCT TRIM(v)::uuid AS entity_id
438
+ FROM unnest(string_to_array(COALESCE(p_EntityIDs, ''), ',')) AS v
439
+ WHERE TRIM(v) ~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$';
440
+ v_is_scoped := EXISTS (SELECT 1 FROM _del_scope);
441
+
442
+ -- metadata-side fields for in-scope, non-virtual entities
443
+ DROP TABLE IF EXISTS _del_ef;
444
+ CREATE TEMP TABLE _del_ef AS
445
+ SELECT ef."ID" AS field_id, ef."EntityID" AS entity_id, ef."Entity"::text AS entity_name, ef."Name"::text AS field_name
446
+ FROM __mj."vwEntityFields" ef
447
+ INNER JOIN __mj."vwEntities" e ON ef."EntityID" = e."ID"
448
+ LEFT JOIN unnest(string_to_array(COALESCE(p_ExcludedSchemaNames, ''), ',')) AS ex(v)
449
+ ON e."SchemaName"::text = TRIM(ex.v)
450
+ WHERE e."VirtualEntity" = FALSE
451
+ AND ex.v IS NULL
452
+ AND (NOT v_is_scoped OR ef."EntityID" IN (SELECT s.entity_id FROM _del_scope s));
453
+
454
+ -- actual columns present in the database
455
+ DROP TABLE IF EXISTS _del_actual;
456
+ CREATE TEMP TABLE _del_actual AS
457
+ SELECT sq."EntityID" AS entity_id, sq."FieldName"::text AS field_name
458
+ FROM __mj."vwSQLColumnsAndEntityFields" sq
459
+ WHERE (NOT v_is_scoped OR sq."EntityID" IN (SELECT s.entity_id FROM _del_scope s));
460
+
461
+ -- orphans: metadata field with no matching DB column
462
+ DROP TABLE IF EXISTS _del_deleted;
463
+ CREATE TEMP TABLE _del_deleted AS
464
+ SELECT ef.field_id, ef.entity_id, ef.entity_name, ef.field_name
465
+ FROM _del_ef ef
466
+ LEFT JOIN _del_actual actual
467
+ ON ef.entity_id = actual.entity_id AND ef.field_name = actual.field_name
468
+ WHERE actual.field_name IS NULL;
469
+
470
+ UPDATE __mj."Entity" ent SET "__mj_UpdatedAt" = now()
471
+ WHERE ent."ID" IN (SELECT DISTINCT d.entity_id FROM _del_deleted d);
472
+
473
+ DELETE FROM __mj."EntityFieldValue" efv
474
+ WHERE efv."EntityFieldID" IN (SELECT d.field_id FROM _del_deleted d);
475
+
476
+ DELETE FROM __mj."EntityField" delf
477
+ WHERE delf."ID" IN (SELECT d.field_id FROM _del_deleted d);
478
+
479
+ RETURN QUERY
480
+ SELECT d.field_id, d.entity_id, d.entity_name, d.field_name FROM _del_deleted d;
481
+
482
+ DROP TABLE IF EXISTS _del_deleted;
483
+ DROP TABLE IF EXISTS _del_actual;
484
+ DROP TABLE IF EXISTS _del_ef;
485
+ DROP TABLE IF EXISTS _del_scope;
486
+ END;
487
+ $func$;
488
+
489
+ -- ----------------------------------------------------------------------------
490
+ -- 6. spSetDefaultColumnWidthWhereNeeded
491
+ -- ----------------------------------------------------------------------------
492
+ DROP FUNCTION IF EXISTS __mj."spSetDefaultColumnWidthWhereNeeded"(TEXT);
493
+ CREATE OR REPLACE FUNCTION __mj."spSetDefaultColumnWidthWhereNeeded"(p_ExcludedSchemaNames TEXT)
494
+ RETURNS void
495
+ LANGUAGE plpgsql AS $func$
496
+ BEGIN
497
+ UPDATE __mj."EntityField" ef SET
498
+ "DefaultColumnWidth" =
499
+ CASE ef."Type"
500
+ WHEN 'int' THEN 50
501
+ WHEN 'datetimeoffset' THEN 100
502
+ WHEN 'money' THEN 100
503
+ WHEN 'nchar' THEN 75
504
+ ELSE 150
505
+ END,
506
+ "__mj_UpdatedAt" = now()
507
+ FROM __mj."Entity" e
508
+ LEFT JOIN unnest(string_to_array(COALESCE(p_ExcludedSchemaNames, ''), ',')) AS ex(v)
509
+ ON e."SchemaName"::text = TRIM(ex.v)
510
+ WHERE ef."EntityID" = e."ID"
511
+ AND ef."DefaultColumnWidth" IS NULL
512
+ AND ex.v IS NULL;
513
+ END;
514
+ $func$;
515
+
516
+ -- ----------------------------------------------------------------------------
517
+ -- 7. spUpdateSchemaInfoFromDatabase
518
+ -- (consumer caches the returned SchemaInfo rows)
519
+ -- ----------------------------------------------------------------------------
520
+ DROP FUNCTION IF EXISTS __mj."spUpdateSchemaInfoFromDatabase"(TEXT);
521
+ CREATE OR REPLACE FUNCTION __mj."spUpdateSchemaInfoFromDatabase"(p_ExcludedSchemaNames TEXT DEFAULT NULL)
522
+ RETURNS SETOF __mj."SchemaInfo"
523
+ LANGUAGE plpgsql AS $func$
524
+ BEGIN
525
+ DROP TABLE IF EXISTS _usi_excluded;
526
+ CREATE TEMP TABLE _usi_excluded AS
527
+ SELECT TRIM(s) AS schema_name
528
+ FROM unnest(string_to_array(COALESCE(p_ExcludedSchemaNames, ''), ',')) AS s
529
+ WHERE TRIM(s) <> '';
530
+
531
+ UPDATE __mj."SchemaInfo" si SET
532
+ "Description" = ss."SchemaDescription",
533
+ "__mj_UpdatedAt" = now()
534
+ FROM __mj."vwSQLSchemas" ss
535
+ WHERE si."SchemaName" = ss."SchemaName"
536
+ AND (si."Description" IS NULL OR si."Description" <> COALESCE(ss."SchemaDescription", ''))
537
+ AND ss."SchemaName" NOT IN (SELECT x.schema_name FROM _usi_excluded x);
538
+
539
+ INSERT INTO __mj."SchemaInfo" ("SchemaName", "EntityIDMin", "EntityIDMax", "Comments", "Description")
540
+ SELECT
541
+ ss."SchemaName",
542
+ 1,
543
+ 999999999,
544
+ 'Auto-created by CodeGen. Please update EntityIDMin and EntityIDMax to appropriate values for this schema.',
545
+ ss."SchemaDescription"
546
+ FROM __mj."vwSQLSchemas" ss
547
+ LEFT JOIN __mj."SchemaInfo" si ON ss."SchemaName" = si."SchemaName"
548
+ WHERE si."ID" IS NULL
549
+ AND ss."SchemaName" NOT IN (SELECT x.schema_name FROM _usi_excluded x);
550
+
551
+ RETURN QUERY
552
+ SELECT si.*
553
+ FROM __mj."SchemaInfo" si
554
+ INNER JOIN __mj."vwSQLSchemas" ss ON si."SchemaName" = ss."SchemaName"
555
+ WHERE ss."SchemaName" NOT IN (SELECT x.schema_name FROM _usi_excluded x);
556
+
557
+ DROP TABLE IF EXISTS _usi_excluded;
558
+ END;
559
+ $func$;
560
+
561
+ -- ----------------------------------------------------------------------------
562
+ -- 8. Fix spUpdateEntityFieldRelatedEntityNameFieldMap
563
+ -- The previous PG port had a one-parameter signature and a body that
564
+ -- referenced an undefined p_EntityFieldID — it could never match CodeGen's
565
+ -- two-argument call nor run. Drop it and create the faithful port.
566
+ -- ----------------------------------------------------------------------------
567
+ DROP FUNCTION IF EXISTS __mj."spUpdateEntityFieldRelatedEntityNameFieldMap"(CHARACTER VARYING);
568
+ DROP FUNCTION IF EXISTS __mj."spUpdateEntityFieldRelatedEntityNameFieldMap"(UUID, CHARACTER VARYING);
569
+ CREATE OR REPLACE FUNCTION __mj."spUpdateEntityFieldRelatedEntityNameFieldMap"(
570
+ p_EntityFieldID UUID,
571
+ p_RelatedEntityNameFieldMap VARCHAR
572
+ )
573
+ RETURNS void
574
+ LANGUAGE plpgsql AS $func$
575
+ BEGIN
576
+ UPDATE __mj."EntityField"
577
+ SET "RelatedEntityNameFieldMap" = p_RelatedEntityNameFieldMap
578
+ WHERE "ID" = p_EntityFieldID;
579
+ END;
580
+ $func$;
581
+
582
+ -- ----------------------------------------------------------------------------
583
+ -- spGetPrimaryKeyForTable — PG port of the SQL Server proc used by
584
+ -- ManageMetadataBase.shouldCreateNewEntity to confirm a new table has a PK
585
+ -- before creating an Entity for it. Self-ensured here (rather than relying on
586
+ -- a migration having run) so a fresh PG database always has it available;
587
+ -- without it, every new entity fails validation with
588
+ -- "function __mj.spGetPrimaryKeyForTable(unknown, unknown) does not exist".
589
+ -- ----------------------------------------------------------------------------
590
+ DROP FUNCTION IF EXISTS __mj."spGetPrimaryKeyForTable"(VARCHAR, VARCHAR);
591
+ CREATE OR REPLACE FUNCTION __mj."spGetPrimaryKeyForTable"(
592
+ p_TableName VARCHAR(255),
593
+ p_SchemaName VARCHAR(255)
594
+ )
595
+ RETURNS TABLE(
596
+ "SchemaName" VARCHAR,
597
+ "TableName" VARCHAR,
598
+ "ColumnName" VARCHAR,
599
+ "DataType" VARCHAR,
600
+ "max_length" INTEGER,
601
+ "precision" INTEGER,
602
+ "scale" INTEGER,
603
+ "is_nullable" BOOLEAN
604
+ ) AS
605
+ $spgetpk$
606
+ BEGIN
607
+ RETURN QUERY
608
+ SELECT
609
+ n.nspname::VARCHAR AS "SchemaName",
610
+ c.relname::VARCHAR AS "TableName",
611
+ a.attname::VARCHAR AS "ColumnName",
612
+ COALESCE(base_t.typname, t.typname)::VARCHAR
613
+ AS "DataType",
614
+ CASE
615
+ WHEN t.typname IN ('varchar', 'bpchar', 'char')
616
+ THEN CASE WHEN a.atttypmod = -1 THEN -1 ELSE a.atttypmod - 4 END
617
+ WHEN t.typname = 'text' THEN -1
618
+ ELSE a.attlen::integer
619
+ END AS "max_length",
620
+ CASE
621
+ WHEN t.typname = 'numeric' AND a.atttypmod <> -1
622
+ THEN ((a.atttypmod - 4) >> 16) & 65535
623
+ ELSE 0
624
+ END AS "precision",
625
+ CASE
626
+ WHEN t.typname = 'numeric' AND a.atttypmod <> -1
627
+ THEN (a.atttypmod - 4) & 65535
628
+ ELSE 0
629
+ END AS "scale",
630
+ (NOT a.attnotnull) AS "is_nullable"
631
+ FROM
632
+ pg_catalog.pg_index i
633
+ INNER JOIN pg_catalog.pg_class c ON c.oid = i.indrelid
634
+ INNER JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
635
+ CROSS JOIN LATERAL unnest(i.indkey) AS cols(col_num)
636
+ INNER JOIN pg_catalog.pg_attribute a
637
+ ON a.attrelid = c.oid AND a.attnum = cols.col_num
638
+ INNER JOIN pg_catalog.pg_type t ON t.oid = a.atttypid
639
+ LEFT JOIN pg_catalog.pg_type base_t
640
+ ON base_t.oid = t.typbasetype AND t.typtype = 'd'
641
+ WHERE
642
+ i.indisprimary = true
643
+ AND c.relname = p_TableName
644
+ AND n.nspname = p_SchemaName
645
+ AND a.attnum > 0
646
+ AND NOT a.attisdropped;
647
+ END;
648
+ $spgetpk$ LANGUAGE plpgsql;
649
+ `;
650
+ //# sourceMappingURL=metadataSupportObjects.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metadataSupportObjects.js","sourceRoot":"","sources":["../../../../src/Database/providers/postgresql/metadataSupportObjects.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,8BAA8B,CAAC,YAAoB;IAC/D,OAAO,4BAA4B,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;AACzE,CAAC;AACD,MAAM,4BAA4B,GAAW,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+mBtD,CAAC"}