@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.
Files changed (57) hide show
  1. package/README.md +2 -0
  2. package/dist/Angular/angular-codegen.js +3 -3
  3. package/dist/Angular/angular-codegen.js.map +1 -1
  4. package/dist/Angular/related-entity-components.js +1 -1
  5. package/dist/Angular/related-entity-components.js.map +1 -1
  6. package/dist/Config/config.js +1 -1
  7. package/dist/Config/config.js.map +1 -1
  8. package/dist/Database/codeGenDatabaseProvider.d.ts +266 -12
  9. package/dist/Database/codeGenDatabaseProvider.d.ts.map +1 -1
  10. package/dist/Database/codeGenDatabaseProvider.js +352 -0
  11. package/dist/Database/codeGenDatabaseProvider.js.map +1 -1
  12. package/dist/Database/manage-metadata.d.ts.map +1 -1
  13. package/dist/Database/manage-metadata.js +86 -87
  14. package/dist/Database/manage-metadata.js.map +1 -1
  15. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts +105 -17
  16. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts.map +1 -1
  17. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js +324 -94
  18. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js.map +1 -1
  19. package/dist/Database/providers/postgresql/viewDependencyCapture.d.ts +99 -0
  20. package/dist/Database/providers/postgresql/viewDependencyCapture.d.ts.map +1 -0
  21. package/dist/Database/providers/postgresql/viewDependencyCapture.js +170 -0
  22. package/dist/Database/providers/postgresql/viewDependencyCapture.js.map +1 -0
  23. package/dist/Database/providers/postgresql/viewFallback.d.ts +75 -0
  24. package/dist/Database/providers/postgresql/viewFallback.d.ts.map +1 -0
  25. package/dist/Database/providers/postgresql/viewFallback.js +172 -0
  26. package/dist/Database/providers/postgresql/viewFallback.js.map +1 -0
  27. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts +0 -25
  28. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts.map +1 -1
  29. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js +9 -113
  30. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js.map +1 -1
  31. package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.test.d.ts +2 -0
  32. package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.test.d.ts.map +1 -0
  33. package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.test.js +207 -0
  34. package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.test.js.map +1 -0
  35. package/dist/Database/sql.d.ts +9 -1
  36. package/dist/Database/sql.d.ts.map +1 -1
  37. package/dist/Database/sql.js +41 -7
  38. package/dist/Database/sql.js.map +1 -1
  39. package/dist/Database/sql_codegen.d.ts +30 -0
  40. package/dist/Database/sql_codegen.d.ts.map +1 -1
  41. package/dist/Database/sql_codegen.js +205 -44
  42. package/dist/Database/sql_codegen.js.map +1 -1
  43. package/dist/Misc/advanced_generation.js +1 -1
  44. package/dist/Misc/advanced_generation.js.map +1 -1
  45. package/dist/Misc/createNewUser.js +2 -2
  46. package/dist/Misc/createNewUser.js.map +1 -1
  47. package/dist/Misc/entity_subclasses_codegen.js +5 -5
  48. package/dist/Misc/entity_subclasses_codegen.js.map +1 -1
  49. package/dist/Misc/graphql_server_codegen.js +6 -6
  50. package/dist/Misc/graphql_server_codegen.js.map +1 -1
  51. package/dist/Misc/sql_logging.d.ts.map +1 -1
  52. package/dist/Misc/sql_logging.js +14 -4
  53. package/dist/Misc/sql_logging.js.map +1 -1
  54. package/dist/runCodeGen.d.ts.map +1 -1
  55. package/dist/runCodeGen.js +66 -2
  56. package/dist/runCodeGen.js.map +1 -1
  57. 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, logWarning } from '../../../Misc/status_logging.js';
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
- // Permissions are handled separately by sql_codegen.ts via generateViewPermissions()
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 ${pgDialect.QuoteSchema(entity.SchemaName, viewName)}
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.${EntityInfo.UpdatedAtFieldName} := NOW() AT TIME ZONE 'UTC';
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
- const paramName = `p_${this.toSnakeCase(ef.CodeName)}`;
505
- const sqlType = this.mapSQLType(ef.SQLFullType);
506
- let defaultVal = '';
507
- if (!isUpdate && ef.IsPrimaryKey && !ef.AutoIncrement) {
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
- else if (!isUpdate && ef.HasDefaultValue && !ef.AllowsNull) {
512
- defaultVal = ' DEFAULT NULL';
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
- * Builds either the column name list or the value expression list for an INSERT
525
- * statement, depending on whether {@link prefix} is empty (column names) or set
526
- * (parameter values with `p_` prefix). Handles special date fields
527
- * (`__mj_CreatedAt`, `__mj_UpdatedAt`) by substituting `NOW() AT TIME ZONE 'UTC'`
528
- * and applies default-value COALESCE wrappers for fields with non-null defaults.
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
- generateInsertFieldString(entity, entityFields, prefix, excludePrimaryKey = false) {
531
- const autoGeneratedPrimaryKey = entity.FirstPrimaryKey.AutoIncrement;
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 can execute multi-statement scripts in a single query call.
989
- // On error, the full batch rolls back to last savepoint; we log and continue.
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
- logWarning(`[CodeGen] SQL warning in ${path.basename(absoluteFilePath)}: ${msg.substring(0, 200)}`);
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
- /** Determines whether a field should be included in CRUD parameters */
1091
- shouldIncludeFieldInParams(ef, isUpdate) {
1092
- const autoGeneratedPrimaryKey = ef.AutoIncrement;
1093
- return ((ef.AllowUpdateAPI || (ef.IsPrimaryKey && isUpdate) || (ef.IsPrimaryKey && !autoGeneratedPrimaryKey && !isUpdate)) &&
1094
- !ef.IsVirtual &&
1095
- (!ef.IsPrimaryKey || !autoGeneratedPrimaryKey || isUpdate) &&
1096
- !ef.IsSpecialDateField);
1097
- }
1098
- /** Determines whether a field should be skipped for INSERT */
1099
- shouldSkipFieldForInsert(ef, autoGeneratedPrimaryKey, excludePrimaryKey) {
1100
- return ((excludePrimaryKey && ef.IsPrimaryKey) ||
1101
- (ef.IsPrimaryKey && autoGeneratedPrimaryKey) ||
1102
- ef.IsVirtual ||
1103
- !ef.AllowUpdateAPI ||
1104
- ef.AutoIncrement);
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 paramName;
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: insertColumns,
1159
- finalValues: insertValues,
1388
+ finalColumns,
1389
+ finalValues,
1160
1390
  };
1161
1391
  }
1162
1392
  /** Builds the DELETE body and return type based on entity delete type */