@memberjunction/codegen-lib 5.30.1 → 5.31.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 +2 -0
- package/dist/Angular/angular-codegen.js +3 -3
- package/dist/Angular/angular-codegen.js.map +1 -1
- package/dist/Angular/related-entity-components.js +1 -1
- package/dist/Angular/related-entity-components.js.map +1 -1
- package/dist/Config/config.js +1 -1
- package/dist/Config/config.js.map +1 -1
- package/dist/Database/codeGenDatabaseProvider.d.ts +266 -12
- package/dist/Database/codeGenDatabaseProvider.d.ts.map +1 -1
- package/dist/Database/codeGenDatabaseProvider.js +352 -0
- package/dist/Database/codeGenDatabaseProvider.js.map +1 -1
- package/dist/Database/manage-metadata.d.ts.map +1 -1
- package/dist/Database/manage-metadata.js +86 -87
- package/dist/Database/manage-metadata.js.map +1 -1
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts +105 -17
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts.map +1 -1
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js +324 -94
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js.map +1 -1
- package/dist/Database/providers/postgresql/viewDependencyCapture.d.ts +99 -0
- package/dist/Database/providers/postgresql/viewDependencyCapture.d.ts.map +1 -0
- package/dist/Database/providers/postgresql/viewDependencyCapture.js +170 -0
- package/dist/Database/providers/postgresql/viewDependencyCapture.js.map +1 -0
- package/dist/Database/providers/postgresql/viewFallback.d.ts +75 -0
- package/dist/Database/providers/postgresql/viewFallback.d.ts.map +1 -0
- package/dist/Database/providers/postgresql/viewFallback.js +172 -0
- package/dist/Database/providers/postgresql/viewFallback.js.map +1 -0
- package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts +0 -25
- package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts.map +1 -1
- package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js +9 -113
- package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js.map +1 -1
- package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.test.d.ts +2 -0
- package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.test.d.ts.map +1 -0
- package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.test.js +207 -0
- package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.test.js.map +1 -0
- package/dist/Database/sql.d.ts +9 -1
- package/dist/Database/sql.d.ts.map +1 -1
- package/dist/Database/sql.js +41 -7
- package/dist/Database/sql.js.map +1 -1
- package/dist/Database/sql_codegen.d.ts +30 -0
- package/dist/Database/sql_codegen.d.ts.map +1 -1
- package/dist/Database/sql_codegen.js +205 -44
- package/dist/Database/sql_codegen.js.map +1 -1
- package/dist/Misc/advanced_generation.js +1 -1
- package/dist/Misc/advanced_generation.js.map +1 -1
- package/dist/Misc/createNewUser.js +2 -2
- package/dist/Misc/createNewUser.js.map +1 -1
- package/dist/Misc/entity_subclasses_codegen.js +5 -5
- package/dist/Misc/entity_subclasses_codegen.js.map +1 -1
- package/dist/Misc/graphql_server_codegen.js +6 -6
- 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 +14 -4
- package/dist/Misc/sql_logging.js.map +1 -1
- package/dist/runCodeGen.d.ts.map +1 -1
- package/dist/runCodeGen.js +66 -2
- package/dist/runCodeGen.js.map +1 -1
- package/package.json +18 -18
|
@@ -9,10 +9,11 @@ import { EntityInfo } from '@memberjunction/core';
|
|
|
9
9
|
import { RegisterClass } from '@memberjunction/global';
|
|
10
10
|
import { CodeGenDatabaseProvider, CRUDType, } from '../../codeGenDatabaseProvider.js';
|
|
11
11
|
import { configInfo } from '../../../Config/config.js';
|
|
12
|
-
import { logError
|
|
12
|
+
import { logError } from '../../../Misc/status_logging.js';
|
|
13
13
|
import { PostgreSQLDialect } from '@memberjunction/sql-dialect';
|
|
14
14
|
import * as fs from 'fs';
|
|
15
15
|
import path from 'path';
|
|
16
|
+
import { executeWithFallback } from './viewFallback.js';
|
|
16
17
|
const pgDialect = new PostgreSQLDialect();
|
|
17
18
|
/**
|
|
18
19
|
* PostgreSQL implementation of the CodeGen database provider.
|
|
@@ -57,6 +58,24 @@ let PostgreSQLCodeGenProvider = class PostgreSQLCodeGenProvider extends CodeGenD
|
|
|
57
58
|
* Generates a PostgreSQL `CREATE OR REPLACE VIEW` statement for an entity's base view.
|
|
58
59
|
* Includes all base table columns, parent/related field joins, and root field lateral
|
|
59
60
|
* joins. Applies a soft-delete `WHERE` filter when the entity uses soft deletes.
|
|
61
|
+
*
|
|
62
|
+
* **Non-destructive strategy.** Historically this method emitted
|
|
63
|
+
* `DROP VIEW IF EXISTS ... CASCADE;` before the CREATE, which let it handle every
|
|
64
|
+
* column-signature change — but also silently destroyed any dependent view, function,
|
|
65
|
+
* trigger, or GRANT on the view. When a later statement (or subsequent entity in the
|
|
66
|
+
* same run) failed, half the database's objects could be gone with no error surfaced.
|
|
67
|
+
*
|
|
68
|
+
* We now emit just `CREATE OR REPLACE VIEW`, which PostgreSQL accepts when the new
|
|
69
|
+
* column list is a prefix of the existing one plus optional trailing additions. Any
|
|
70
|
+
* incompatible change (rename, reorder, type change, removed column) fails with
|
|
71
|
+
* SQLSTATE `42P16 invalid_table_definition`. That error is intentionally allowed to
|
|
72
|
+
* propagate up through `executeSQLFileViaShell` → the per-entity batch loop → and is
|
|
73
|
+
* surfaced as a real regeneration failure. A follow-up pass will add a capture-drop-
|
|
74
|
+
* restore fallback for 42P16 that preserves dependents via `pg_depend` and
|
|
75
|
+
* `pg_get_viewdef` / `pg_get_functiondef`. Until that lands, 42P16 on PG is loud and
|
|
76
|
+
* actionable instead of silent and destructive.
|
|
77
|
+
*
|
|
78
|
+
* Permissions are handled separately by sql_codegen.ts via generateViewPermissions().
|
|
60
79
|
*/
|
|
61
80
|
generateBaseView(context) {
|
|
62
81
|
const { entity } = context;
|
|
@@ -65,7 +84,7 @@ let PostgreSQLCodeGenProvider = class PostgreSQLCodeGenProvider extends CodeGenD
|
|
|
65
84
|
const whereClause = this.buildSoftDeleteWhereClause(entity, alias);
|
|
66
85
|
const selectParts = this.buildBaseViewSelectParts(context, alias);
|
|
67
86
|
const fromParts = this.buildBaseViewFromParts(context, entity, alias);
|
|
68
|
-
|
|
87
|
+
const quotedView = pgDialect.QuoteSchema(entity.SchemaName, viewName);
|
|
69
88
|
return `
|
|
70
89
|
------------------------------------------------------------
|
|
71
90
|
----- BASE VIEW FOR ENTITY: ${entity.Name}
|
|
@@ -73,7 +92,7 @@ let PostgreSQLCodeGenProvider = class PostgreSQLCodeGenProvider extends CodeGenD
|
|
|
73
92
|
----- BASE TABLE: ${entity.BaseTable}
|
|
74
93
|
----- PRIMARY KEY: ${entity.PrimaryKeys.map((pk) => pk.Name).join(', ')}
|
|
75
94
|
------------------------------------------------------------
|
|
76
|
-
CREATE OR REPLACE VIEW ${
|
|
95
|
+
CREATE OR REPLACE VIEW ${quotedView}
|
|
77
96
|
AS
|
|
78
97
|
SELECT
|
|
79
98
|
${selectParts}
|
|
@@ -83,6 +102,31 @@ ${whereClause};
|
|
|
83
102
|
`;
|
|
84
103
|
}
|
|
85
104
|
// ─── CRUD CREATE ─────────────────────────────────────────────────────
|
|
105
|
+
/**
|
|
106
|
+
* Emits a `DO` block that drops every existing overload of `<schema>.<fnName>`.
|
|
107
|
+
* Run before each `CREATE OR REPLACE FUNCTION` to prevent PG's "function name is
|
|
108
|
+
* not unique" error when the parameter list grows (e.g. a new column adds a new
|
|
109
|
+
* `DEFAULT NULL` parameter — old + new overloads become call-ambiguous and PG
|
|
110
|
+
* refuses both `CREATE OR REPLACE` and any caller dispatch). Iterating overloads
|
|
111
|
+
* via `pg_proc` is the only way to drop without knowing the exact prior signature.
|
|
112
|
+
*/
|
|
113
|
+
generateDropAllOverloadsBlock(schemaName, fnName) {
|
|
114
|
+
// Single quotes inside the DO block need to escape the surrounding quoting.
|
|
115
|
+
// We use dollar-quoted strings to keep the body readable.
|
|
116
|
+
return `DO $do$
|
|
117
|
+
DECLARE r RECORD;
|
|
118
|
+
BEGIN
|
|
119
|
+
FOR r IN SELECT oid::regprocedure AS sig
|
|
120
|
+
FROM pg_proc
|
|
121
|
+
WHERE proname = '${fnName}'
|
|
122
|
+
AND pronamespace = '${schemaName}'::regnamespace
|
|
123
|
+
LOOP
|
|
124
|
+
EXECUTE 'DROP FUNCTION ' || r.sig::text;
|
|
125
|
+
END LOOP;
|
|
126
|
+
END
|
|
127
|
+
$do$;
|
|
128
|
+
`;
|
|
129
|
+
}
|
|
86
130
|
/**
|
|
87
131
|
* Generates a PostgreSQL `CREATE OR REPLACE FUNCTION` for inserting a new record.
|
|
88
132
|
* The function accepts typed parameters for each writable field, performs an `INSERT`
|
|
@@ -90,25 +134,36 @@ ${whereClause};
|
|
|
90
134
|
* `RETURN QUERY SELECT`. Handles auto-increment PKs (using `RETURNING ... INTO`),
|
|
91
135
|
* UUID PKs (with `COALESCE` to gen_random_uuid()), and composite PKs. Also emits
|
|
92
136
|
* `GRANT EXECUTE` permissions for authorized roles.
|
|
137
|
+
*
|
|
138
|
+
* Prepends a DROP-all-overloads block (see `generateDropAllOverloadsBlock`) so
|
|
139
|
+
* adding/removing a column doesn't trigger PG's overload-ambiguity error.
|
|
93
140
|
*/
|
|
94
141
|
generateCRUDCreate(entity) {
|
|
95
142
|
const fnName = this.getCRUDRoutineName(entity, CRUDType.Create);
|
|
96
143
|
const viewName = this.getBaseViewName(entity);
|
|
97
144
|
const paramString = this.generateCRUDParamString(entity.Fields, false);
|
|
98
145
|
const permissions = this.generateCRUDPermissions(entity, fnName, CRUDType.Create);
|
|
99
|
-
const insertColumns = this.generateInsertFieldString(entity, entity.Fields, '', false);
|
|
100
|
-
const insertValues = this.generateInsertFieldString(entity, entity.Fields, 'p_', false);
|
|
101
146
|
const firstKey = entity.FirstPrimaryKey;
|
|
147
|
+
// For UUID PKs and AutoIncrement PKs, the strategy below adds the PK
|
|
148
|
+
// column manually (with v_new_id or RETURNING). Excluding it from the
|
|
149
|
+
// auto-generated insertColumns/insertValues avoids the column appearing
|
|
150
|
+
// twice in the INSERT statement.
|
|
151
|
+
const pkType = firstKey.Type.toLowerCase().trim();
|
|
152
|
+
const pkHandledByStrategy = firstKey.AutoIncrement ||
|
|
153
|
+
((pkType === 'uniqueidentifier' || pkType === 'uuid') && entity.PrimaryKeys.length === 1);
|
|
154
|
+
const insertColumns = this.generateInsertFieldString(entity, entity.Fields, '', pkHandledByStrategy);
|
|
155
|
+
const insertValues = this.generateInsertFieldString(entity, entity.Fields, 'p_', pkHandledByStrategy);
|
|
102
156
|
const strategy = this.buildCreateInsertStrategy(entity, firstKey, insertColumns, insertValues);
|
|
103
157
|
return `
|
|
104
158
|
------------------------------------------------------------
|
|
105
159
|
----- CREATE FUNCTION FOR ${entity.BaseTable}
|
|
106
160
|
------------------------------------------------------------
|
|
161
|
+
${this.generateDropAllOverloadsBlock(entity.SchemaName, fnName)}
|
|
107
162
|
CREATE OR REPLACE FUNCTION ${pgDialect.QuoteSchema(entity.SchemaName, fnName)}(
|
|
108
163
|
${paramString}
|
|
109
164
|
) RETURNS SETOF ${pgDialect.QuoteSchema(entity.SchemaName, viewName)} AS $$
|
|
110
165
|
DECLARE
|
|
111
|
-
v_new_id ${firstKey.SQLFullType};
|
|
166
|
+
v_new_id ${this.mapSQLType(firstKey.SQLFullType)};
|
|
112
167
|
BEGIN
|
|
113
168
|
${strategy.preInsert}INSERT INTO ${pgDialect.QuoteSchema(entity.SchemaName, entity.BaseTable)}
|
|
114
169
|
(
|
|
@@ -149,6 +204,7 @@ ${permissions}
|
|
|
149
204
|
------------------------------------------------------------
|
|
150
205
|
----- UPDATE FUNCTION FOR ${entity.BaseTable}
|
|
151
206
|
------------------------------------------------------------
|
|
207
|
+
${this.generateDropAllOverloadsBlock(entity.SchemaName, fnName)}
|
|
152
208
|
CREATE OR REPLACE FUNCTION ${pgDialect.QuoteSchema(entity.SchemaName, fnName)}(
|
|
153
209
|
${paramString}
|
|
154
210
|
) RETURNS SETOF ${pgDialect.QuoteSchema(entity.SchemaName, viewName)} AS $$
|
|
@@ -192,16 +248,18 @@ ${trigger}
|
|
|
192
248
|
const fnName = this.getCRUDRoutineName(entity, CRUDType.Delete);
|
|
193
249
|
const permissions = this.generateCRUDPermissions(entity, fnName, CRUDType.Delete);
|
|
194
250
|
const { paramDecl, deleteBody, returnType, returnStatement } = this.buildDeleteStrategy(entity, cascadeSQL);
|
|
251
|
+
const needsRecordVar = cascadeSQL.includes('v_rec');
|
|
195
252
|
return `
|
|
196
253
|
------------------------------------------------------------
|
|
197
254
|
----- DELETE FUNCTION FOR ${entity.BaseTable}
|
|
198
255
|
------------------------------------------------------------
|
|
256
|
+
${this.generateDropAllOverloadsBlock(entity.SchemaName, fnName)}
|
|
199
257
|
CREATE OR REPLACE FUNCTION ${pgDialect.QuoteSchema(entity.SchemaName, fnName)}(
|
|
200
258
|
${paramDecl}
|
|
201
259
|
) RETURNS ${returnType} AS $$
|
|
202
260
|
#variable_conflict use_column
|
|
203
261
|
DECLARE
|
|
204
|
-
v_affected_count INTEGER;
|
|
262
|
+
v_affected_count INTEGER;${needsRecordVar ? '\n v_rec RECORD;' : ''}
|
|
205
263
|
BEGIN
|
|
206
264
|
${cascadeSQL}
|
|
207
265
|
${deleteBody}
|
|
@@ -235,7 +293,7 @@ ${permissions}
|
|
|
235
293
|
CREATE OR REPLACE FUNCTION ${pgDialect.QuoteSchema(entity.SchemaName, trigFnName)}()
|
|
236
294
|
RETURNS TRIGGER AS $$
|
|
237
295
|
BEGIN
|
|
238
|
-
NEW
|
|
296
|
+
NEW."${EntityInfo.UpdatedAtFieldName}" := NOW() AT TIME ZONE 'UTC';
|
|
239
297
|
RETURN NEW;
|
|
240
298
|
END;
|
|
241
299
|
$$ LANGUAGE plpgsql;
|
|
@@ -495,67 +553,64 @@ END $$;
|
|
|
495
553
|
* to allow optional arguments, and PostgreSQL's requirement that all subsequent
|
|
496
554
|
* parameters also have defaults once the first default appears is respected.
|
|
497
555
|
*/
|
|
556
|
+
/**
|
|
557
|
+
* PostgreSQL parameter types are mapped from the entity field's
|
|
558
|
+
* T-SQL `SQLFullType` (the metadata is canonically T-SQL) into the
|
|
559
|
+
* equivalent PostgreSQL type. SQL Server's base-class default would
|
|
560
|
+
* pass the type through unchanged, which would emit invalid PG
|
|
561
|
+
* function signatures.
|
|
562
|
+
*/
|
|
563
|
+
renderParameterType(ef) {
|
|
564
|
+
return this.mapSQLType(ef.SQLFullType);
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* PostgreSQL override: same tolerant-SP shape as the base class, but
|
|
568
|
+
* with PG's "all params after the first DEFAULT must also have DEFAULTs"
|
|
569
|
+
* rule enforced. Once any parameter becomes optional (or a `_Clear`
|
|
570
|
+
* companion is emitted), every subsequent parameter is forced to
|
|
571
|
+
* `DEFAULT NULL` even if it would otherwise be required, because PG
|
|
572
|
+
* function signatures don't allow gaps between defaulted params.
|
|
573
|
+
*
|
|
574
|
+
* All decision logic and dialect-syntax bits route through the same
|
|
575
|
+
* base-class helpers / dialect methods used by SQL Server — the only
|
|
576
|
+
* thing that differs is the sticky-defaults walk.
|
|
577
|
+
*/
|
|
498
578
|
generateCRUDParamString(entityFields, isUpdate) {
|
|
579
|
+
const dialect = this.Dialect;
|
|
580
|
+
const nullDefault = dialect.ParameterDefault(dialect.NullLiteral);
|
|
499
581
|
const parts = [];
|
|
500
582
|
let foundDefault = false;
|
|
501
583
|
for (const ef of entityFields) {
|
|
502
584
|
if (!this.shouldIncludeFieldInParams(ef, isUpdate))
|
|
503
585
|
continue;
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
defaultVal = ' DEFAULT NULL';
|
|
586
|
+
// _Clear companion is emitted immediately before its main parameter
|
|
587
|
+
// for nullable columns whose database default is non-NULL.
|
|
588
|
+
if (!ef.IsPrimaryKey && this.needsClearCompanion(ef)) {
|
|
589
|
+
parts.push(`${dialect.ParameterRef(ef.CodeName + '_Clear')} bit${dialect.ParameterDefault('0')}`);
|
|
509
590
|
foundDefault = true;
|
|
510
591
|
}
|
|
511
|
-
|
|
512
|
-
|
|
592
|
+
const isOptional = !this.isParamRequired(ef, isUpdate);
|
|
593
|
+
// Sticky-defaults: once any param has a default, all subsequent params
|
|
594
|
+
// must also be defaulted in PG. We default them to NULL.
|
|
595
|
+
const needsStickyDefault = foundDefault && !isOptional;
|
|
596
|
+
const defaultClause = isOptional || needsStickyDefault ? nullDefault : '';
|
|
597
|
+
if (defaultClause)
|
|
513
598
|
foundDefault = true;
|
|
514
|
-
}
|
|
515
|
-
else if (!isUpdate && foundDefault) {
|
|
516
|
-
// PG requires all params after the first DEFAULT to also have DEFAULTs
|
|
517
|
-
defaultVal = ' DEFAULT NULL';
|
|
518
|
-
}
|
|
519
|
-
parts.push(`${paramName} ${sqlType}${defaultVal}`);
|
|
599
|
+
parts.push(`${dialect.ParameterRef(ef.CodeName)} ${this.renderParameterType(ef)}${defaultClause}`);
|
|
520
600
|
}
|
|
521
601
|
return parts.join(',\n ');
|
|
522
602
|
}
|
|
523
603
|
/**
|
|
524
|
-
*
|
|
525
|
-
*
|
|
526
|
-
*
|
|
527
|
-
*
|
|
528
|
-
*
|
|
604
|
+
* PostgreSQL override of the INSERT default-value renderer to handle
|
|
605
|
+
* the BIT→BOOLEAN type-strictness gap. The base implementation passes
|
|
606
|
+
* the SQL Server-style default through unchanged, but PG's strict
|
|
607
|
+
* typing means `COALESCE(boolean_param, 0)` fails with "operator does
|
|
608
|
+
* not exist: boolean = integer". This override remaps `BIT` defaults
|
|
609
|
+
* (`0`/`1`) to `BOOLEAN` literals (`FALSE`/`TRUE`) when the field's
|
|
610
|
+
* underlying type is bit/boolean.
|
|
529
611
|
*/
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
const parts = [];
|
|
533
|
-
for (const ef of entityFields) {
|
|
534
|
-
if (this.shouldSkipFieldForInsert(ef, autoGeneratedPrimaryKey, excludePrimaryKey))
|
|
535
|
-
continue;
|
|
536
|
-
if (prefix !== '' && ef.IsSpecialDateField) {
|
|
537
|
-
parts.push(this.getSpecialDateInsertValue(ef));
|
|
538
|
-
}
|
|
539
|
-
else if (prefix === '') {
|
|
540
|
-
// Column name side
|
|
541
|
-
parts.push(pgDialect.QuoteIdentifier(ef.Name));
|
|
542
|
-
}
|
|
543
|
-
else {
|
|
544
|
-
// Parameter value side
|
|
545
|
-
parts.push(this.getParameterInsertValue(ef, prefix));
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
return parts.join(',\n ');
|
|
549
|
-
}
|
|
550
|
-
/** @inheritdoc */
|
|
551
|
-
generateUpdateFieldString(entityFields) {
|
|
552
|
-
const parts = [];
|
|
553
|
-
for (const ef of entityFields) {
|
|
554
|
-
if (ef.IsPrimaryKey || ef.IsVirtual || !ef.AllowUpdateAPI || ef.AutoIncrement || ef.IsSpecialDateField)
|
|
555
|
-
continue;
|
|
556
|
-
parts.push(`${pgDialect.QuoteIdentifier(ef.Name)} = p_${this.toSnakeCase(ef.CodeName)}`);
|
|
557
|
-
}
|
|
558
|
-
return parts.join(',\n ');
|
|
612
|
+
formatInsertDefaultValue(ef) {
|
|
613
|
+
return this.formatBooleanCompatibleDefault(ef);
|
|
559
614
|
}
|
|
560
615
|
// ─── ROUTINE NAMING ──────────────────────────────────────────────────
|
|
561
616
|
/** @inheritdoc */
|
|
@@ -844,8 +899,9 @@ ORDER BY ordinal_position`;
|
|
|
844
899
|
'OBJECT_ID', 'SCOPE_IDENTITY',
|
|
845
900
|
// Aggregate / scalar functions
|
|
846
901
|
'COUNT', 'MAX', 'MIN', 'SUM', 'AVG', 'COALESCE', 'CAST', 'CONVERT', 'ISNULL',
|
|
847
|
-
'LEN', 'DATALENGTH', 'LOWER', 'UPPER', 'LTRIM', 'RTRIM', 'TRIM', 'REPLACE',
|
|
902
|
+
'LEN', 'LENGTH', 'DATALENGTH', 'LOWER', 'UPPER', 'LTRIM', 'RTRIM', 'TRIM', 'REPLACE',
|
|
848
903
|
'SUBSTRING', 'CHARINDEX', 'PATINDEX', 'STUFF', 'CONCAT', 'FORMAT',
|
|
904
|
+
'LEFT', 'RIGHT', 'POSITION', 'OVERLAY', 'EXTRACT', 'GREATEST', 'LEAST',
|
|
849
905
|
'DATEADD', 'DATEDIFF', 'DATEPART', 'YEAR', 'MONTH', 'DAY', 'HOUR', 'MINUTE',
|
|
850
906
|
'SECOND', 'NOW', 'CURRENT_TIMESTAMP',
|
|
851
907
|
// PostgreSQL specific
|
|
@@ -985,16 +1041,22 @@ ORDER BY ordinal_position`;
|
|
|
985
1041
|
});
|
|
986
1042
|
try {
|
|
987
1043
|
await client.connect();
|
|
988
|
-
// Postgres
|
|
989
|
-
//
|
|
1044
|
+
// Postgres executes a multi-statement script in a single query call. A single
|
|
1045
|
+
// statement error aborts the rest of the batch server-side (simple query
|
|
1046
|
+
// protocol) — so silently converting that to `return true` hid real data loss:
|
|
1047
|
+
// CodeGen-generated DROP VIEW CASCADE would succeed, the follow-up CREATE
|
|
1048
|
+
// would fail, and the caller would believe everything was fine. Return false
|
|
1049
|
+
// on error and log it as an error, not a warning, so the per-entity batching
|
|
1050
|
+
// loops can aggregate and (in strict mode) halt the install.
|
|
990
1051
|
try {
|
|
991
1052
|
await client.query(sql);
|
|
1053
|
+
return true;
|
|
992
1054
|
}
|
|
993
1055
|
catch (err) {
|
|
994
1056
|
const msg = err instanceof Error ? err.message : String(err);
|
|
995
|
-
|
|
1057
|
+
logError(`[CodeGen] SQL execution failed in ${path.basename(absoluteFilePath)}: ${msg.substring(0, 400)}`);
|
|
1058
|
+
return false;
|
|
996
1059
|
}
|
|
997
|
-
return true;
|
|
998
1060
|
}
|
|
999
1061
|
catch (e) {
|
|
1000
1062
|
logError(`[CodeGen] Failed to execute SQL file ${absoluteFilePath}: ${e instanceof Error ? e.message : e}`);
|
|
@@ -1007,6 +1069,175 @@ ORDER BY ordinal_position`;
|
|
|
1007
1069
|
catch { /* best-effort cleanup */ }
|
|
1008
1070
|
}
|
|
1009
1071
|
}
|
|
1072
|
+
/** @inheritdoc */
|
|
1073
|
+
getRoutineNamesBySchemaSQL(schemas) {
|
|
1074
|
+
if (schemas.length === 0)
|
|
1075
|
+
return '';
|
|
1076
|
+
// pg_proc holds every function (PL/pgSQL, SQL, C, etc.). prokind filters:
|
|
1077
|
+
// 'f' = normal function, 'p' = procedure, 'a' = aggregate, 'w' = window.
|
|
1078
|
+
// CodeGen emits 'f' (CRUD functions, root-ID TVFs, FTS functions) and 'p'
|
|
1079
|
+
// is unused on PG today but cheap to include in case we add procedures later.
|
|
1080
|
+
const inList = schemas.map(s => `'${s.replace(/'/g, "''")}'`).join(', ');
|
|
1081
|
+
return `SELECT n.nspname AS schema_name, p.proname AS routine_name
|
|
1082
|
+
FROM pg_proc p
|
|
1083
|
+
JOIN pg_namespace n ON n.oid = p.pronamespace
|
|
1084
|
+
WHERE p.prokind IN ('f', 'p')
|
|
1085
|
+
AND n.nspname IN (${inList});`;
|
|
1086
|
+
}
|
|
1087
|
+
/**
|
|
1088
|
+
* PG-specific base-view regeneration with 42P16 recovery.
|
|
1089
|
+
*
|
|
1090
|
+
* Runs the provided `CREATE OR REPLACE VIEW` SQL through `executeWithFallback`
|
|
1091
|
+
* on a dedicated connection: happy path issues the CREATE OR REPLACE directly,
|
|
1092
|
+
* and only on SQLSTATE 42P16 does the capture/drop/recreate/restore dance fire
|
|
1093
|
+
* inside a transaction that preserves every dependent view, function, grant,
|
|
1094
|
+
* comment, and owner. See `viewFallback.ts` for the contract.
|
|
1095
|
+
*
|
|
1096
|
+
* `willRegenerate` is passed through so dependents CodeGen is about to rebuild
|
|
1097
|
+
* in the same run are skipped at restore time (avoids restoring a stale
|
|
1098
|
+
* captured definition against a newly-regenerated target).
|
|
1099
|
+
*/
|
|
1100
|
+
async regenerateBaseView(entity, viewSQL, willRegenerate) {
|
|
1101
|
+
const pgHost = process.env.PG_HOST ?? configInfo.dbHost;
|
|
1102
|
+
const pgPort = Number(process.env.PG_PORT ?? configInfo.dbPort ?? 5432);
|
|
1103
|
+
const pgDatabase = process.env.PG_DATABASE ?? configInfo.dbDatabase;
|
|
1104
|
+
const pgUser = process.env.PG_USERNAME ?? configInfo.codeGenLogin;
|
|
1105
|
+
const pgPassword = process.env.PG_PASSWORD ?? configInfo.codeGenPassword;
|
|
1106
|
+
if (!pgUser || !pgPassword || !pgDatabase) {
|
|
1107
|
+
throw new Error('PostgreSQL user, password, and database must be provided in the configuration or environment variables');
|
|
1108
|
+
}
|
|
1109
|
+
const pgModule = await import('pg');
|
|
1110
|
+
const client = new pgModule.default.Client({
|
|
1111
|
+
host: pgHost,
|
|
1112
|
+
port: pgPort,
|
|
1113
|
+
user: pgUser,
|
|
1114
|
+
password: pgPassword,
|
|
1115
|
+
database: pgDatabase,
|
|
1116
|
+
});
|
|
1117
|
+
await client.connect();
|
|
1118
|
+
try {
|
|
1119
|
+
await executeWithFallback({
|
|
1120
|
+
client,
|
|
1121
|
+
schema: entity.SchemaName,
|
|
1122
|
+
viewName: entity.BaseView,
|
|
1123
|
+
createOrReplaceSQL: viewSQL,
|
|
1124
|
+
willRegenerate,
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
finally {
|
|
1128
|
+
try {
|
|
1129
|
+
await client.end();
|
|
1130
|
+
}
|
|
1131
|
+
catch { /* best-effort cleanup */ }
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* Phased per-entity execution for PG. Runs view → CRUD functions → view
|
|
1136
|
+
* permissions against the target DB, guaranteeing phase 2 is skipped if
|
|
1137
|
+
* phase 1 failed (so we never leave `fn_create_*` functions pointing at
|
|
1138
|
+
* a missing or stale view's rowtype).
|
|
1139
|
+
*
|
|
1140
|
+
* Phase 1 routes through `executeWithFallback` so a 42P16 triggers the
|
|
1141
|
+
* capture/drop/recreate/restore flow rather than blowing up. Phase 2 runs
|
|
1142
|
+
* each CRUD function's CREATE individually — we do NOT concatenate them
|
|
1143
|
+
* because node-pg's simple query protocol would then abort the whole
|
|
1144
|
+
* batch on the first failure; running them separately gives a per-routine
|
|
1145
|
+
* error signal. Phase 3 applies view-level GRANTs.
|
|
1146
|
+
*/
|
|
1147
|
+
async executeEntityPhased(opts) {
|
|
1148
|
+
const pgHost = process.env.PG_HOST ?? configInfo.dbHost;
|
|
1149
|
+
const pgPort = Number(process.env.PG_PORT ?? configInfo.dbPort ?? 5432);
|
|
1150
|
+
const pgDatabase = process.env.PG_DATABASE ?? configInfo.dbDatabase;
|
|
1151
|
+
const pgUser = process.env.PG_USERNAME ?? configInfo.codeGenLogin;
|
|
1152
|
+
const pgPassword = process.env.PG_PASSWORD ?? configInfo.codeGenPassword;
|
|
1153
|
+
if (!pgUser || !pgPassword || !pgDatabase) {
|
|
1154
|
+
throw new Error('PostgreSQL user, password, and database must be provided in the configuration or environment variables');
|
|
1155
|
+
}
|
|
1156
|
+
const pgModule = await import('pg');
|
|
1157
|
+
const client = new pgModule.default.Client({
|
|
1158
|
+
host: pgHost,
|
|
1159
|
+
port: pgPort,
|
|
1160
|
+
user: pgUser,
|
|
1161
|
+
password: pgPassword,
|
|
1162
|
+
database: pgDatabase,
|
|
1163
|
+
});
|
|
1164
|
+
await client.connect();
|
|
1165
|
+
try {
|
|
1166
|
+
// ── Phase 0: root-ID TVFs ────────────────────────────────────
|
|
1167
|
+
// The base view references these helper functions; PG rejects
|
|
1168
|
+
// CREATE VIEW with `function does not exist` if they aren't
|
|
1169
|
+
// installed first. Run as a single batch — these are independent
|
|
1170
|
+
// CREATE OR REPLACE FUNCTION statements separated by GO.
|
|
1171
|
+
if (opts.tvfSQL && opts.tvfSQL.trim()) {
|
|
1172
|
+
try {
|
|
1173
|
+
await client.query(opts.tvfSQL);
|
|
1174
|
+
}
|
|
1175
|
+
catch (e) {
|
|
1176
|
+
return {
|
|
1177
|
+
success: false,
|
|
1178
|
+
phase: 'tvf',
|
|
1179
|
+
error: e instanceof Error ? e : new Error(String(e)),
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
// ── Phase 1: base view (fallback-aware for 42P16) ────────────
|
|
1184
|
+
if (opts.viewSQL && opts.viewSQL.trim()) {
|
|
1185
|
+
try {
|
|
1186
|
+
await executeWithFallback({
|
|
1187
|
+
client,
|
|
1188
|
+
schema: opts.entity.SchemaName,
|
|
1189
|
+
viewName: opts.entity.BaseView,
|
|
1190
|
+
createOrReplaceSQL: opts.viewSQL,
|
|
1191
|
+
willRegenerate: opts.willRegenerate,
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
catch (e) {
|
|
1195
|
+
return {
|
|
1196
|
+
success: false,
|
|
1197
|
+
phase: 'view',
|
|
1198
|
+
error: e instanceof Error ? e : new Error(String(e)),
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
// ── Phase 2: CRUD functions (skipped if phase 1 failed) ──────
|
|
1203
|
+
// Run each CREATE FUNCTION separately so per-function errors
|
|
1204
|
+
// don't abort the others via simple-query-protocol semantics.
|
|
1205
|
+
for (const crudSQL of [opts.crudCreateSQL, opts.crudUpdateSQL, opts.crudDeleteSQL]) {
|
|
1206
|
+
if (!crudSQL || !crudSQL.trim())
|
|
1207
|
+
continue;
|
|
1208
|
+
try {
|
|
1209
|
+
await client.query(crudSQL);
|
|
1210
|
+
}
|
|
1211
|
+
catch (e) {
|
|
1212
|
+
return {
|
|
1213
|
+
success: false,
|
|
1214
|
+
phase: 'functions',
|
|
1215
|
+
error: e instanceof Error ? e : new Error(String(e)),
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
// ── Phase 3: view permissions ────────────────────────────────
|
|
1220
|
+
if (opts.viewPermSQL && opts.viewPermSQL.trim()) {
|
|
1221
|
+
try {
|
|
1222
|
+
await client.query(opts.viewPermSQL);
|
|
1223
|
+
}
|
|
1224
|
+
catch (e) {
|
|
1225
|
+
return {
|
|
1226
|
+
success: false,
|
|
1227
|
+
phase: 'permissions',
|
|
1228
|
+
error: e instanceof Error ? e : new Error(String(e)),
|
|
1229
|
+
};
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
return { success: true, phase: null };
|
|
1233
|
+
}
|
|
1234
|
+
finally {
|
|
1235
|
+
try {
|
|
1236
|
+
await client.end();
|
|
1237
|
+
}
|
|
1238
|
+
catch { /* best-effort cleanup */ }
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1010
1241
|
// ═══════════════════════════════════════════════════════════════════════
|
|
1011
1242
|
// PRIVATE HELPERS
|
|
1012
1243
|
// ═══════════════════════════════════════════════════════════════════════
|
|
@@ -1087,39 +1318,23 @@ ORDER BY ordinal_position`;
|
|
|
1087
1318
|
joins.push(context.rootJoins);
|
|
1088
1319
|
return joins.length > 0 ? '\n' + joins.join('\n') : '';
|
|
1089
1320
|
}
|
|
1090
|
-
/**
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
(
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
}
|
|
1106
|
-
/** Gets the INSERT value for a special date field */
|
|
1107
|
-
getSpecialDateInsertValue(ef) {
|
|
1108
|
-
if (ef.IsCreatedAtField || ef.IsUpdatedAtField)
|
|
1109
|
-
return "NOW() AT TIME ZONE 'UTC'";
|
|
1110
|
-
return 'NULL'; // DeletedAt
|
|
1111
|
-
}
|
|
1112
|
-
/** Gets the parameter insert value, handling defaults */
|
|
1113
|
-
getParameterInsertValue(ef, prefix) {
|
|
1114
|
-
const paramName = `${prefix}${this.toSnakeCase(ef.CodeName)}`;
|
|
1115
|
-
if (ef.HasDefaultValue && !ef.AllowsNull) {
|
|
1116
|
-
const formattedDefault = this.formatDefaultValue(ef.DefaultValue, ef.NeedsQuotes);
|
|
1117
|
-
if (ef.IsUniqueIdentifier) {
|
|
1118
|
-
return `CASE WHEN ${paramName} = '00000000-0000-0000-0000-000000000000'::UUID THEN ${formattedDefault} ELSE COALESCE(${paramName}, ${formattedDefault}) END`;
|
|
1119
|
-
}
|
|
1120
|
-
return `COALESCE(${paramName}, ${formattedDefault})`;
|
|
1321
|
+
/**
|
|
1322
|
+
* Like formatDefaultValue, but maps SQL Server BIT defaults (0/1) to PG
|
|
1323
|
+
* BOOLEAN literals (FALSE/TRUE) when the field type is bit/boolean.
|
|
1324
|
+
* COALESCE() in PG is type-strict — passing `COALESCE(boolean_param, 0)`
|
|
1325
|
+
* fails with "types boolean and integer cannot be matched".
|
|
1326
|
+
*/
|
|
1327
|
+
formatBooleanCompatibleDefault(ef) {
|
|
1328
|
+
const formatted = this.formatDefaultValue(ef.DefaultValue, ef.NeedsQuotes);
|
|
1329
|
+
const fieldType = ef.Type.toLowerCase().trim();
|
|
1330
|
+
if (fieldType === 'bit' || fieldType === 'boolean' || fieldType === 'bool') {
|
|
1331
|
+
const t = formatted.trim().toLowerCase();
|
|
1332
|
+
if (t === '0' || t === "'0'")
|
|
1333
|
+
return 'FALSE';
|
|
1334
|
+
if (t === '1' || t === "'1'")
|
|
1335
|
+
return 'TRUE';
|
|
1121
1336
|
}
|
|
1122
|
-
return
|
|
1337
|
+
return formatted;
|
|
1123
1338
|
}
|
|
1124
1339
|
/** Builds a WHERE clause using primary key fields with a parameter prefix */
|
|
1125
1340
|
buildPrimaryKeyWhereClause(entity, prefix) {
|
|
@@ -1151,12 +1366,27 @@ ORDER BY ordinal_position`;
|
|
|
1151
1366
|
}
|
|
1152
1367
|
// Composite keys or non-auto, non-UUID PKs
|
|
1153
1368
|
const selectWhere = entity.PrimaryKeys.map((k) => `${pgDialect.QuoteIdentifier(k.Name)} = p_${this.toSnakeCase(k.CodeName)}`).join(' AND ');
|
|
1369
|
+
// Composite-PK tables: every PK column has AllowUpdateAPI=0, so generateInsertFieldString
|
|
1370
|
+
// filters them all out. Prepend them to finalColumns/finalValues so the INSERT is valid.
|
|
1371
|
+
// (The single-PK uniqueidentifier case is already handled above via v_new_id.)
|
|
1372
|
+
let finalColumns = insertColumns;
|
|
1373
|
+
let finalValues = insertValues;
|
|
1374
|
+
if (entity.PrimaryKeys.length > 1) {
|
|
1375
|
+
const pkColumns = entity.PrimaryKeys
|
|
1376
|
+
.map((k) => pgDialect.QuoteIdentifier(k.Name))
|
|
1377
|
+
.join(',\n ');
|
|
1378
|
+
const pkValues = entity.PrimaryKeys
|
|
1379
|
+
.map((k) => `p_${this.toSnakeCase(k.CodeName)}`)
|
|
1380
|
+
.join(',\n ');
|
|
1381
|
+
finalColumns = `${pkColumns},\n ${insertColumns}`;
|
|
1382
|
+
finalValues = `${pkValues},\n ${insertValues}`;
|
|
1383
|
+
}
|
|
1154
1384
|
return {
|
|
1155
1385
|
preInsert: '',
|
|
1156
1386
|
returningClause: '',
|
|
1157
1387
|
selectClause: `SELECT * FROM ${pgDialect.QuoteSchema(entity.SchemaName, viewName)}\n WHERE ${selectWhere}`,
|
|
1158
|
-
finalColumns
|
|
1159
|
-
finalValues
|
|
1388
|
+
finalColumns,
|
|
1389
|
+
finalValues,
|
|
1160
1390
|
};
|
|
1161
1391
|
}
|
|
1162
1392
|
/** Builds the DELETE body and return type based on entity delete type */
|