@memberjunction/codegen-lib 6.1.0-edge.2 → 6.1.0-edge.4

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 (133) hide show
  1. package/LICENSE +180 -4
  2. package/README.md +89 -43
  3. package/dist/Angular/angular-codegen.d.ts +0 -8
  4. package/dist/Angular/angular-codegen.d.ts.map +1 -1
  5. package/dist/Angular/angular-codegen.js +32 -22
  6. package/dist/Angular/angular-codegen.js.map +1 -1
  7. package/dist/Angular/entity-data-grid-related-entity-component.js +2 -2
  8. package/dist/Angular/entity-data-grid-related-entity-component.js.map +1 -1
  9. package/dist/Angular/related-entity-components.d.ts +1 -1
  10. package/dist/Angular/related-entity-components.js +1 -1
  11. package/dist/Config/config.d.ts +118 -1
  12. package/dist/Config/config.d.ts.map +1 -1
  13. package/dist/Config/config.js +202 -8
  14. package/dist/Config/config.js.map +1 -1
  15. package/dist/Database/__tests__/exclude-schema-permissions.test.d.ts +2 -0
  16. package/dist/Database/__tests__/exclude-schema-permissions.test.d.ts.map +1 -0
  17. package/dist/Database/__tests__/exclude-schema-permissions.test.js +32 -0
  18. package/dist/Database/__tests__/exclude-schema-permissions.test.js.map +1 -0
  19. package/dist/Database/codeGenDatabaseProvider.d.ts +178 -2
  20. package/dist/Database/codeGenDatabaseProvider.d.ts.map +1 -1
  21. package/dist/Database/codeGenDatabaseProvider.js +175 -10
  22. package/dist/Database/codeGenDatabaseProvider.js.map +1 -1
  23. package/dist/Database/heal-schema-params.d.ts +33 -0
  24. package/dist/Database/heal-schema-params.d.ts.map +1 -0
  25. package/dist/Database/heal-schema-params.js +51 -0
  26. package/dist/Database/heal-schema-params.js.map +1 -0
  27. package/dist/Database/manage-metadata.d.ts +301 -2
  28. package/dist/Database/manage-metadata.d.ts.map +1 -1
  29. package/dist/Database/manage-metadata.js +1242 -96
  30. package/dist/Database/manage-metadata.js.map +1 -1
  31. package/dist/Database/materializationAnalysis.d.ts +241 -0
  32. package/dist/Database/materializationAnalysis.d.ts.map +1 -0
  33. package/dist/Database/materializationAnalysis.js +415 -0
  34. package/dist/Database/materializationAnalysis.js.map +1 -0
  35. package/dist/Database/materializationBroadRender.d.ts +67 -0
  36. package/dist/Database/materializationBroadRender.d.ts.map +1 -0
  37. package/dist/Database/materializationBroadRender.js +104 -0
  38. package/dist/Database/materializationBroadRender.js.map +1 -0
  39. package/dist/Database/materializationDrift.d.ts +59 -0
  40. package/dist/Database/materializationDrift.d.ts.map +1 -0
  41. package/dist/Database/materializationDrift.js +85 -0
  42. package/dist/Database/materializationDrift.js.map +1 -0
  43. package/dist/Database/materializationParamClassifier.d.ts +92 -0
  44. package/dist/Database/materializationParamClassifier.d.ts.map +1 -0
  45. package/dist/Database/materializationParamClassifier.js +224 -0
  46. package/dist/Database/materializationParamClassifier.js.map +1 -0
  47. package/dist/Database/materializationParamVerifier.d.ts +71 -0
  48. package/dist/Database/materializationParamVerifier.d.ts.map +1 -0
  49. package/dist/Database/materializationParamVerifier.js +302 -0
  50. package/dist/Database/materializationParamVerifier.js.map +1 -0
  51. package/dist/Database/materializationSqlAst.d.ts +78 -0
  52. package/dist/Database/materializationSqlAst.d.ts.map +1 -0
  53. package/dist/Database/materializationSqlAst.js +142 -0
  54. package/dist/Database/materializationSqlAst.js.map +1 -0
  55. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts +93 -56
  56. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts.map +1 -1
  57. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js +399 -247
  58. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js.map +1 -1
  59. package/dist/Database/providers/postgresql/__tests__/PostgreSQLCodeGenProvider.materialization.test.d.ts +2 -0
  60. package/dist/Database/providers/postgresql/__tests__/PostgreSQLCodeGenProvider.materialization.test.d.ts.map +1 -0
  61. package/dist/Database/providers/postgresql/__tests__/PostgreSQLCodeGenProvider.materialization.test.js +119 -0
  62. package/dist/Database/providers/postgresql/__tests__/PostgreSQLCodeGenProvider.materialization.test.js.map +1 -0
  63. package/dist/Database/providers/postgresql/__tests__/PostgreSQLHierarchyFunctions.test.d.ts +2 -0
  64. package/dist/Database/providers/postgresql/__tests__/PostgreSQLHierarchyFunctions.test.d.ts.map +1 -0
  65. package/dist/Database/providers/postgresql/__tests__/PostgreSQLHierarchyFunctions.test.js +155 -0
  66. package/dist/Database/providers/postgresql/__tests__/PostgreSQLHierarchyFunctions.test.js.map +1 -0
  67. package/dist/Database/providers/postgresql/__tests__/SoftPrimaryKeyIndex.test.d.ts +2 -0
  68. package/dist/Database/providers/postgresql/__tests__/SoftPrimaryKeyIndex.test.d.ts.map +1 -0
  69. package/dist/Database/providers/postgresql/__tests__/SoftPrimaryKeyIndex.test.js +110 -0
  70. package/dist/Database/providers/postgresql/__tests__/SoftPrimaryKeyIndex.test.js.map +1 -0
  71. package/dist/Database/providers/postgresql/metadataSupportObjects.js +78 -10
  72. package/dist/Database/providers/postgresql/metadataSupportObjects.js.map +1 -1
  73. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts +72 -1
  74. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts.map +1 -1
  75. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js +322 -0
  76. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js.map +1 -1
  77. package/dist/Database/providers/sqlserver/__tests__/BaseViewEmissionGoldenMaster.test.d.ts +2 -0
  78. package/dist/Database/providers/sqlserver/__tests__/BaseViewEmissionGoldenMaster.test.d.ts.map +1 -0
  79. package/dist/Database/providers/sqlserver/__tests__/BaseViewEmissionGoldenMaster.test.js +547 -0
  80. package/dist/Database/providers/sqlserver/__tests__/BaseViewEmissionGoldenMaster.test.js.map +1 -0
  81. package/dist/Database/providers/sqlserver/__tests__/BaseViewRegenDecision.test.d.ts +2 -0
  82. package/dist/Database/providers/sqlserver/__tests__/BaseViewRegenDecision.test.d.ts.map +1 -0
  83. package/dist/Database/providers/sqlserver/__tests__/BaseViewRegenDecision.test.js +721 -0
  84. package/dist/Database/providers/sqlserver/__tests__/BaseViewRegenDecision.test.js.map +1 -0
  85. package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.materialization.test.d.ts +2 -0
  86. package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.materialization.test.d.ts.map +1 -0
  87. package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.materialization.test.js +78 -0
  88. package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.materialization.test.js.map +1 -0
  89. package/dist/Database/providers/sqlserver/__tests__/SQLServerHierarchyTVF.test.d.ts +2 -0
  90. package/dist/Database/providers/sqlserver/__tests__/SQLServerHierarchyTVF.test.d.ts.map +1 -0
  91. package/dist/Database/providers/sqlserver/__tests__/SQLServerHierarchyTVF.test.js +155 -0
  92. package/dist/Database/providers/sqlserver/__tests__/SQLServerHierarchyTVF.test.js.map +1 -0
  93. package/dist/Database/providers/sqlserver/__tests__/SoftPrimaryKeyIndex.test.d.ts +2 -0
  94. package/dist/Database/providers/sqlserver/__tests__/SoftPrimaryKeyIndex.test.d.ts.map +1 -0
  95. package/dist/Database/providers/sqlserver/__tests__/SoftPrimaryKeyIndex.test.js +195 -0
  96. package/dist/Database/providers/sqlserver/__tests__/SoftPrimaryKeyIndex.test.js.map +1 -0
  97. package/dist/Database/schema-filters.d.ts +23 -0
  98. package/dist/Database/schema-filters.d.ts.map +1 -0
  99. package/dist/Database/schema-filters.js +42 -0
  100. package/dist/Database/schema-filters.js.map +1 -0
  101. package/dist/Database/schema-scope.d.ts.map +1 -1
  102. package/dist/Database/schema-scope.js +4 -0
  103. package/dist/Database/schema-scope.js.map +1 -1
  104. package/dist/Database/sql_codegen.d.ts +39 -5
  105. package/dist/Database/sql_codegen.d.ts.map +1 -1
  106. package/dist/Database/sql_codegen.js +176 -67
  107. package/dist/Database/sql_codegen.js.map +1 -1
  108. package/dist/EntityNameScanner/EntityNameScanner.d.ts.map +1 -1
  109. package/dist/EntityNameScanner/EntityNameScanner.js +33 -2
  110. package/dist/EntityNameScanner/EntityNameScanner.js.map +1 -1
  111. package/dist/Manifest/GenerateClassRegistrationsManifest.d.ts +8 -4
  112. package/dist/Manifest/GenerateClassRegistrationsManifest.d.ts.map +1 -1
  113. package/dist/Manifest/GenerateClassRegistrationsManifest.js +63 -20
  114. package/dist/Manifest/GenerateClassRegistrationsManifest.js.map +1 -1
  115. package/dist/Misc/action_subclasses_codegen.d.ts.map +1 -1
  116. package/dist/Misc/action_subclasses_codegen.js +4 -4
  117. package/dist/Misc/action_subclasses_codegen.js.map +1 -1
  118. package/dist/Misc/entity_subclasses_codegen.d.ts +61 -0
  119. package/dist/Misc/entity_subclasses_codegen.d.ts.map +1 -1
  120. package/dist/Misc/entity_subclasses_codegen.js +437 -158
  121. package/dist/Misc/entity_subclasses_codegen.js.map +1 -1
  122. package/dist/Misc/graphql_server_codegen.d.ts +4 -58
  123. package/dist/Misc/graphql_server_codegen.d.ts.map +1 -1
  124. package/dist/Misc/graphql_server_codegen.js +22 -236
  125. package/dist/Misc/graphql_server_codegen.js.map +1 -1
  126. package/dist/Misc/sql_logging.d.ts +25 -0
  127. package/dist/Misc/sql_logging.d.ts.map +1 -1
  128. package/dist/Misc/sql_logging.js +51 -8
  129. package/dist/Misc/sql_logging.js.map +1 -1
  130. package/dist/Misc/system_integrity.d.ts.map +1 -1
  131. package/dist/Misc/system_integrity.js +5 -0
  132. package/dist/Misc/system_integrity.js.map +1 -1
  133. package/package.json +30 -30
@@ -4,14 +4,13 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- var PostgreSQLCodeGenProvider_1;
8
7
  import { EntityInfo, ResolveStartupMode, SetProvider, StartupManager } from '@memberjunction/core';
9
8
  import { RegisterClass, UUIDsEqual } from '@memberjunction/global';
10
9
  import { CodeGenDatabaseProvider, CRUDType, } from '../../codeGenDatabaseProvider.js';
11
10
  import { configInfo, mj_core_schema } from '../../../Config/config.js';
12
11
  import { logError, startSpinner, succeedSpinner } from '../../../Misc/status_logging.js';
13
12
  import { buildMetadataSupportObjectsSQL } from './metadataSupportObjects.js';
14
- import { PostgreSQLDialect } from '@memberjunction/sql-dialect';
13
+ import { PostgreSQLDialect, AutoQuotePostgreSQLIdentifiers, restarLayeredOuterView, buildCreateOrReplaceLayeredOuterViewSQL, LayeredOuterRestarError } from '@memberjunction/sql-dialect';
15
14
  import { shouldIncludeFieldInParams, useJsonArgShape, UserCache, } from '@memberjunction/generic-database-provider';
16
15
  import { POSTGRESQL_PROCEDURE_PARAM_LIMIT, PostgreSQLDataProvider, PostgreSQLProviderConfigData, } from '@memberjunction/postgresql-dataprovider';
17
16
  import { PGConnection, getPgConfig } from '../../../Config/pg-connection.js';
@@ -30,7 +29,6 @@ const pgDialect = new PostgreSQLDialect();
30
29
  * `ClassFactory.CreateInstance(CodeGenDatabaseProvider, configInfo.dbPlatform)`.
31
30
  */
32
31
  let PostgreSQLCodeGenProvider = class PostgreSQLCodeGenProvider extends CodeGenDatabaseProvider {
33
- static { PostgreSQLCodeGenProvider_1 = this; }
34
32
  /** @inheritdoc */
35
33
  get Dialect() {
36
34
  return pgDialect;
@@ -172,7 +170,6 @@ let PostgreSQLCodeGenProvider = class PostgreSQLCodeGenProvider extends CodeGenD
172
170
  */
173
171
  generateBaseView(context) {
174
172
  const { entity } = context;
175
- this.assertLayeredBaseViewSupported(entity);
176
173
  // The GENERATED view — `GeneratedViewName` is BaseView unless the entity layers a custom view
177
174
  // over an inner generated one, in which case CodeGen writes the inner name. The CRUD
178
175
  // routines keep using getBaseViewName(): they return rows from the PUBLIC view, so a
@@ -382,6 +379,63 @@ EXCEPTION WHEN invalid_table_definition THEN
382
379
  END $vw_regen$;
383
380
  `;
384
381
  }
382
+ // ─── MATERIALIZATION ─────────────────────────────────────────────────
383
+ /**
384
+ * PostgreSQL materialized-table DDL. The create is **conditional** (`CREATE TABLE IF NOT
385
+ * EXISTS`) so a migration-provided `materialized_<name>` table with bespoke indexing is
386
+ * detected and reused rather than clobbered (plan §12). Emits the single-column surrogate
387
+ * PRIMARY KEY, which is itself the required unique index.
388
+ *
389
+ * PG dialect vs. SQL Server: double-quoted identifiers, `CREATE TABLE IF NOT EXISTS` in
390
+ * place of the `IF OBJECT_ID(...) IS NULL` guard, and the surrogate column carries PG's
391
+ * `GENERATED ALWAYS AS IDENTITY` clause (see {@link getMaterializedSurrogateColumnType}).
392
+ */
393
+ generateMaterializedTableSQL(schema, tableName, columns) {
394
+ // MJ entity-field metadata stores CANONICAL (SQL Server-flavored) type names — e.g. 'uniqueidentifier',
395
+ // 'nvarchar(50)' — regardless of the physical DB dialect. Every other PG DDL path maps these via
396
+ // mapSQLType (see spCreate/spUpdate generation); the materialized-table DDL must do the same, or a
397
+ // base-view/query materialization emits `... uniqueidentifier ...` and PG fails the CREATE TABLE with
398
+ // `type "uniqueidentifier" does not exist`, aborting the whole codegen run.
399
+ const colLines = columns.map((c) => ` ${pgDialect.QuoteIdentifier(c.Name)} ${this.mapSQLType(c.SQLType)} ${c.Nullable ? 'NULL' : 'NOT NULL'}`);
400
+ const pkCols = columns.filter((c) => c.IsPrimaryKey).map((c) => c.Name);
401
+ const pkClause = pkCols.length
402
+ ? `,\n CONSTRAINT ${pgDialect.QuoteIdentifier(`PK_${tableName}`)} PRIMARY KEY (${pkCols
403
+ .map((n) => pgDialect.QuoteIdentifier(n))
404
+ .join(', ')})`
405
+ : '';
406
+ return `CREATE TABLE IF NOT EXISTS ${pgDialect.QuoteSchema(schema, tableName)} (
407
+ ${colLines.join(',\n')}${pkClause}
408
+ );`;
409
+ }
410
+ /**
411
+ * PostgreSQL wrapper-view DDL — the stable read contract over the materialized table.
412
+ * Uses `CREATE OR REPLACE VIEW` so the same statement both creates the view and atomically
413
+ * repoints it at a freshly-built table during refresh (plan §11.2). The body is always
414
+ * `SELECT * FROM <table>` and the shadow table shares the column shape, so PG's
415
+ * `CREATE OR REPLACE` column-compatibility rule is satisfied on the swap.
416
+ */
417
+ generateMaterializedWrapperViewSQL(schema, viewName, tableName) {
418
+ return `CREATE OR REPLACE VIEW ${pgDialect.QuoteSchema(schema, viewName)}
419
+ AS
420
+ SELECT * FROM ${pgDialect.QuoteSchema(schema, tableName)};`;
421
+ }
422
+ /**
423
+ * PostgreSQL synthetic surrogate key: a SQL-standard auto-assigned identity column
424
+ * (`GENERATED ALWAYS AS IDENTITY`), `bigint` for headroom on large materialized sets.
425
+ * v1 full-rebuild only; the deterministic combined-key hashing in §5 replaces this in Phase 3.
426
+ */
427
+ getMaterializedSurrogateColumnType() {
428
+ return 'bigint GENERATED ALWAYS AS IDENTITY';
429
+ }
430
+ /**
431
+ * PostgreSQL KEYED surrogate key type: the combined-key SHA-256 hash the refresh writes comes from
432
+ * `encode(digest(…), 'hex')`, whose type is `text`, and the full rebuild's `CREATE TABLE AS` infers
433
+ * exactly that. Typing the minted entity's PK column `text` MATCHES the post-refresh physical column,
434
+ * avoiding the int-vs-hash divergence between CodeGen metadata and the rebuilt table.
435
+ */
436
+ getMaterializedHashSurrogateColumnType() {
437
+ return 'text';
438
+ }
385
439
  // ─── CRUD CREATE ─────────────────────────────────────────────────────
386
440
  /**
387
441
  * Emits a `DO` block that drops every existing overload of `<schema>.<fnName>`.
@@ -869,6 +923,26 @@ EXECUTE FUNCTION ${pgDialect.QuoteSchema(entity.SchemaName, trigFnName)}();
869
923
  return (`CREATE INDEX IF NOT EXISTS ${pgDialect.QuoteIdentifier(indexName)}\n` +
870
924
  ` ON ${pgDialect.QuoteSchema(entity.SchemaName, entity.BaseTable)} (${pgDialect.QuoteIdentifier(f.Name)});`);
871
925
  }
926
+ softPrimaryKeyIndexPrefix() {
927
+ return 'idx_auto_mj_softpk_';
928
+ }
929
+ /**
930
+ * PostgreSQL will happily accept an unbounded `text` column in a btree key and then fail at
931
+ * INSERT time if a value exceeds roughly an eighth of a page — an error that surfaces long
932
+ * after the index was created, on whichever row happens to be too long. Treating unbounded
933
+ * columns as unindexable keeps the failure at generation time, where it names the column.
934
+ */
935
+ isIndexableKeyColumn(f) {
936
+ return f.Length !== -1;
937
+ }
938
+ formatCompositeIndexStatement(entity, fields, indexName) {
939
+ const cols = fields.map(f => pgDialect.QuoteIdentifier(f.Name)).join(', ');
940
+ return (`-- Index for the soft primary key (${fields.map(f => f.Name).join(', ')}) in table ${entity.BaseTable}.\n` +
941
+ `-- The key is metadata-only — no PRIMARY KEY constraint — so without this the per-record\n` +
942
+ `-- existence check on the create path scans the whole table.\n` +
943
+ `CREATE INDEX IF NOT EXISTS ${pgDialect.QuoteIdentifier(indexName)}\n` +
944
+ ` ON ${pgDialect.QuoteSchema(entity.SchemaName, entity.BaseTable)} (${cols});`);
945
+ }
872
946
  // ─── FULL-TEXT SEARCH ────────────────────────────────────────────────
873
947
  /**
874
948
  * Generates a complete PostgreSQL full-text search infrastructure for an entity.
@@ -955,7 +1029,231 @@ WHERE ${ftsColName} IS NULL;
955
1029
  * the parent FK upward, capped at 100 levels to prevent infinite loops. Returns the
956
1030
  * root record's primary key value.
957
1031
  */
1032
+ // ─── RECURSIVE HIERARCHY FUNCTIONS ──────────────────────────────────
1033
+ /**
1034
+ * Generates a PostgreSQL function returning a table of hierarchy metadata
1035
+ * (RootID, Depth, Path, IsLeaf, ChildCount) for a self-referencing foreign key.
1036
+ */
1037
+ generateHierarchyMetaFunction(entity, field) {
1038
+ if (entity.PrimaryKeys.length !== 1) {
1039
+ throw new Error(`[Hierarchy] Entity '${entity.Name}' has ${entity.PrimaryKeys.length} primary key fields. MemberJunction hierarchy TVF generation requires a single-column primary key.`);
1040
+ }
1041
+ const primaryKey = entity.FirstPrimaryKey.Name;
1042
+ const primaryKeyType = this.mapSQLType(entity.FirstPrimaryKey.SQLFullType);
1043
+ const fieldName = field.Name;
1044
+ const fnName = this.getHierarchyMetaFunctionName(entity, field);
1045
+ return `
1046
+ ------------------------------------------------------------
1047
+ ----- HIERARCHY METADATA FUNCTION FOR: ${entity.BaseTable}.${fieldName}
1048
+ ------------------------------------------------------------
1049
+ CREATE OR REPLACE FUNCTION ${pgDialect.QuoteSchema(entity.SchemaName, fnName)}(
1050
+ p_record_id ${primaryKeyType},
1051
+ p_parent_id ${primaryKeyType}
1052
+ ) RETURNS TABLE (
1053
+ "RootID" ${primaryKeyType},
1054
+ "Depth" INTEGER,
1055
+ "Path" TEXT,
1056
+ "IsLeaf" BOOLEAN,
1057
+ "ChildCount" INTEGER
1058
+ ) AS $$
1059
+ WITH RECURSIVE cte_ancestors AS (
1060
+ SELECT
1061
+ ${pgDialect.QuoteIdentifier(primaryKey)},
1062
+ ${pgDialect.QuoteIdentifier(fieldName)},
1063
+ 0 AS depth,
1064
+ '/' || ${pgDialect.QuoteIdentifier(primaryKey)}::TEXT || '/' AS path
1065
+ FROM
1066
+ ${pgDialect.QuoteSchema(entity.SchemaName, entity.BaseTable)}
1067
+ WHERE
1068
+ ${pgDialect.QuoteIdentifier(primaryKey)} = p_record_id
1069
+
1070
+ UNION ALL
1071
+
1072
+ SELECT
1073
+ p.${pgDialect.QuoteIdentifier(primaryKey)},
1074
+ p.${pgDialect.QuoteIdentifier(fieldName)},
1075
+ c.depth + 1 AS depth,
1076
+ '/' || p.${pgDialect.QuoteIdentifier(primaryKey)}::TEXT || c.path AS path
1077
+ FROM
1078
+ ${pgDialect.QuoteSchema(entity.SchemaName, entity.BaseTable)} p
1079
+ INNER JOIN
1080
+ cte_ancestors c ON p.${pgDialect.QuoteIdentifier(primaryKey)} = c.${pgDialect.QuoteIdentifier(fieldName)}
1081
+ WHERE
1082
+ c.depth < 100
1083
+ )
1084
+ SELECT
1085
+ a.${pgDialect.QuoteIdentifier(primaryKey)} AS "RootID",
1086
+ (SELECT MAX(depth) FROM cte_ancestors)::INTEGER AS "Depth",
1087
+ (SELECT path FROM cte_ancestors ORDER BY depth DESC LIMIT 1)::TEXT AS "Path",
1088
+ (NOT EXISTS (SELECT 1 FROM ${pgDialect.QuoteSchema(entity.SchemaName, entity.BaseTable)} WHERE ${pgDialect.QuoteIdentifier(fieldName)} = p_record_id))::BOOLEAN AS "IsLeaf",
1089
+ (SELECT COUNT(1)::INTEGER FROM ${pgDialect.QuoteSchema(entity.SchemaName, entity.BaseTable)} WHERE ${pgDialect.QuoteIdentifier(fieldName)} = p_record_id) AS "ChildCount"
1090
+ FROM
1091
+ cte_ancestors a
1092
+ WHERE
1093
+ a.${pgDialect.QuoteIdentifier(fieldName)} IS NULL OR p_parent_id IS NULL
1094
+ ORDER BY
1095
+ a.depth DESC
1096
+ LIMIT 1;
1097
+ $$ LANGUAGE sql STABLE;
1098
+ `;
1099
+ }
1100
+ /**
1101
+ * Generates a PostgreSQL table-valued function returning all descendant records of a root node.
1102
+ */
1103
+ generateDescendantsFunction(entity, field) {
1104
+ if (entity.PrimaryKeys.length !== 1) {
1105
+ throw new Error(`[Hierarchy] Entity '${entity.Name}' has ${entity.PrimaryKeys.length} primary key fields. MemberJunction hierarchy TVF generation requires a single-column primary key.`);
1106
+ }
1107
+ const primaryKey = entity.FirstPrimaryKey.Name;
1108
+ const primaryKeyType = this.mapSQLType(entity.FirstPrimaryKey.SQLFullType);
1109
+ const fieldName = field.Name;
1110
+ const fnName = this.getDescendantsFunctionName(entity, field);
1111
+ return `
1112
+ ------------------------------------------------------------
1113
+ ----- DESCENDANTS FUNCTION FOR: ${entity.BaseTable}.${fieldName}
1114
+ ------------------------------------------------------------
1115
+ CREATE OR REPLACE FUNCTION ${pgDialect.QuoteSchema(entity.SchemaName, fnName)}(
1116
+ p_root_id ${primaryKeyType},
1117
+ p_max_depth INTEGER DEFAULT NULL
1118
+ ) RETURNS TABLE (
1119
+ "ID" ${primaryKeyType},
1120
+ "Depth" INTEGER,
1121
+ "Path" TEXT,
1122
+ "IsLeaf" BOOLEAN,
1123
+ "ChildCount" INTEGER
1124
+ ) AS $$
1125
+ WITH RECURSIVE cte_descendants AS (
1126
+ SELECT
1127
+ ${pgDialect.QuoteIdentifier(primaryKey)},
1128
+ ${pgDialect.QuoteIdentifier(fieldName)},
1129
+ 0 AS relative_depth,
1130
+ '/' || ${pgDialect.QuoteIdentifier(primaryKey)}::TEXT || '/' AS path
1131
+ FROM
1132
+ ${pgDialect.QuoteSchema(entity.SchemaName, entity.BaseTable)}
1133
+ WHERE
1134
+ ${pgDialect.QuoteIdentifier(primaryKey)} = p_root_id
1135
+
1136
+ UNION ALL
1137
+
1138
+ SELECT
1139
+ c.${pgDialect.QuoteIdentifier(primaryKey)},
1140
+ c.${pgDialect.QuoteIdentifier(fieldName)},
1141
+ p.relative_depth + 1 AS relative_depth,
1142
+ p.path || c.${pgDialect.QuoteIdentifier(primaryKey)}::TEXT || '/' AS path
1143
+ FROM
1144
+ ${pgDialect.QuoteSchema(entity.SchemaName, entity.BaseTable)} c
1145
+ INNER JOIN
1146
+ cte_descendants p ON c.${pgDialect.QuoteIdentifier(fieldName)} = p.${pgDialect.QuoteIdentifier(primaryKey)}
1147
+ WHERE
1148
+ (p_max_depth IS NULL OR p.relative_depth < p_max_depth)
1149
+ AND p.relative_depth < 100
1150
+ )
1151
+ SELECT
1152
+ d.${pgDialect.QuoteIdentifier(primaryKey)} AS "ID",
1153
+ d.relative_depth AS "Depth",
1154
+ d.path AS "Path",
1155
+ (NOT EXISTS (SELECT 1 FROM ${pgDialect.QuoteSchema(entity.SchemaName, entity.BaseTable)} WHERE ${pgDialect.QuoteIdentifier(fieldName)} = d.${pgDialect.QuoteIdentifier(primaryKey)}))::BOOLEAN AS "IsLeaf",
1156
+ (SELECT COUNT(1)::INTEGER FROM ${pgDialect.QuoteSchema(entity.SchemaName, entity.BaseTable)} WHERE ${pgDialect.QuoteIdentifier(fieldName)} = d.${pgDialect.QuoteIdentifier(primaryKey)}) AS "ChildCount"
1157
+ FROM
1158
+ cte_descendants d;
1159
+ $$ LANGUAGE sql STABLE;
1160
+ `;
1161
+ }
1162
+ /**
1163
+ * Generates a PostgreSQL table-valued function returning all ancestors of a node walking upward.
1164
+ */
1165
+ generateAncestorsFunction(entity, field) {
1166
+ if (entity.PrimaryKeys.length !== 1) {
1167
+ throw new Error(`[Hierarchy] Entity '${entity.Name}' has ${entity.PrimaryKeys.length} primary key fields. MemberJunction hierarchy TVF generation requires a single-column primary key.`);
1168
+ }
1169
+ const primaryKey = entity.FirstPrimaryKey.Name;
1170
+ const primaryKeyType = this.mapSQLType(entity.FirstPrimaryKey.SQLFullType);
1171
+ const fieldName = field.Name;
1172
+ const fnName = this.getAncestorsFunctionName(entity, field);
1173
+ return `
1174
+ ------------------------------------------------------------
1175
+ ----- ANCESTORS FUNCTION FOR: ${entity.BaseTable}.${fieldName}
1176
+ ------------------------------------------------------------
1177
+ CREATE OR REPLACE FUNCTION ${pgDialect.QuoteSchema(entity.SchemaName, fnName)}(
1178
+ p_record_id ${primaryKeyType}
1179
+ ) RETURNS TABLE (
1180
+ "ID" ${primaryKeyType},
1181
+ "LevelUp" INTEGER,
1182
+ "Path" TEXT
1183
+ ) AS $$
1184
+ WITH RECURSIVE cte_ancestors AS (
1185
+ SELECT
1186
+ ${pgDialect.QuoteIdentifier(primaryKey)},
1187
+ ${pgDialect.QuoteIdentifier(fieldName)},
1188
+ 0 AS level_up,
1189
+ '/' || ${pgDialect.QuoteIdentifier(primaryKey)}::TEXT || '/' AS path
1190
+ FROM
1191
+ ${pgDialect.QuoteSchema(entity.SchemaName, entity.BaseTable)}
1192
+ WHERE
1193
+ ${pgDialect.QuoteIdentifier(primaryKey)} = p_record_id
1194
+
1195
+ UNION ALL
1196
+
1197
+ SELECT
1198
+ p.${pgDialect.QuoteIdentifier(primaryKey)},
1199
+ p.${pgDialect.QuoteIdentifier(fieldName)},
1200
+ c.level_up + 1 AS level_up,
1201
+ '/' || p.${pgDialect.QuoteIdentifier(primaryKey)}::TEXT || c.path AS path
1202
+ FROM
1203
+ ${pgDialect.QuoteSchema(entity.SchemaName, entity.BaseTable)} p
1204
+ INNER JOIN
1205
+ cte_ancestors c ON p.${pgDialect.QuoteIdentifier(primaryKey)} = c.${pgDialect.QuoteIdentifier(fieldName)}
1206
+ WHERE
1207
+ c.level_up < 100
1208
+ )
1209
+ SELECT
1210
+ a.${pgDialect.QuoteIdentifier(primaryKey)} AS "ID",
1211
+ a.level_up AS "LevelUp",
1212
+ a.path AS "Path"
1213
+ FROM
1214
+ cte_ancestors a;
1215
+ $$ LANGUAGE sql STABLE;
1216
+ `;
1217
+ }
1218
+ getHierarchyMetaFunctionName(entity, field) {
1219
+ return `fn_${this.toSnakeCase(entity.BaseTable)}_${this.toSnakeCase(field.Name)}_get_hierarchy_meta`;
1220
+ }
1221
+ getDescendantsFunctionName(entity, field) {
1222
+ return `fn_${this.toSnakeCase(entity.BaseTable)}_${this.toSnakeCase(field.Name)}_get_descendants`;
1223
+ }
1224
+ getAncestorsFunctionName(entity, field) {
1225
+ return `fn_${this.toSnakeCase(entity.BaseTable)}_${this.toSnakeCase(field.Name)}_get_ancestors`;
1226
+ }
1227
+ /** @inheritdoc */
1228
+ generateHierarchyFieldSelect(_entity, field, alias) {
1229
+ const rootFieldName = `Root${field.Name}`;
1230
+ const depthFieldName = `${field.Name}Depth`;
1231
+ const pathFieldName = `${field.Name}Path`;
1232
+ const isLeafFieldName = `${field.Name}IsLeaf`;
1233
+ const childCountFieldName = `${field.Name}ChildCount`;
1234
+ return `${alias}."RootID" AS ${pgDialect.QuoteIdentifier(rootFieldName)},
1235
+ ${alias}."Depth" AS ${pgDialect.QuoteIdentifier(depthFieldName)},
1236
+ ${alias}."Path" AS ${pgDialect.QuoteIdentifier(pathFieldName)},
1237
+ ${alias}."IsLeaf" AS ${pgDialect.QuoteIdentifier(isLeafFieldName)},
1238
+ ${alias}."ChildCount" AS ${pgDialect.QuoteIdentifier(childCountFieldName)}`;
1239
+ }
1240
+ /**
1241
+ * Generates a `LEFT JOIN LATERAL` clause that invokes the hierarchy metadata function
1242
+ * for a self-referencing field.
1243
+ */
1244
+ generateHierarchyFieldJoin(entity, field, alias) {
1245
+ const fnName = this.getHierarchyMetaFunctionName(entity, field);
1246
+ const tableAlias = entity.BaseTableCodeName.charAt(0).toLowerCase();
1247
+ return `LEFT JOIN LATERAL ${pgDialect.QuoteSchema(entity.SchemaName, fnName)}(${tableAlias}.${pgDialect.QuoteIdentifier(entity.FirstPrimaryKey.Name)}, ${tableAlias}.${pgDialect.QuoteIdentifier(field.Name)}) AS ${alias} ON true`;
1248
+ }
1249
+ /**
1250
+ * Generates a PostgreSQL function that recursively calculates the root record ID for a
1251
+ * self-referencing foreign key.
1252
+ */
958
1253
  generateRootIDFunction(entity, field) {
1254
+ if (entity.PrimaryKeys.length !== 1) {
1255
+ throw new Error(`[Hierarchy] Entity '${entity.Name}' has ${entity.PrimaryKeys.length} primary key fields. MemberJunction hierarchy TVF generation requires a single-column primary key.`);
1256
+ }
959
1257
  const primaryKey = entity.FirstPrimaryKey.Name;
960
1258
  const primaryKeyType = this.mapSQLType(entity.FirstPrimaryKey.SQLFullType);
961
1259
  const fieldName = field.Name;
@@ -1451,18 +1749,10 @@ ORDER BY ordinal_position`;
1451
1749
  return ['information_schema', 'pg_catalog', 'pg_toast', 'pg_temp_1', 'pg_toast_temp_1'];
1452
1750
  }
1453
1751
  /**
1454
- * PostgreSQL has no view-refresh mechanism, so this always returns `false`.
1455
- *
1456
- * NOT because PG views track their source automatically — they do not. PG expands `SELECT *`
1457
- * into an explicit column list at creation and freezes it; a view gains a new underlying column
1458
- * only when the view itself is recreated. CodeGen gets away without a refresh step because it
1459
- * emits every generated view with an explicit column list and re-issues `CREATE OR REPLACE` on
1460
- * every run, so the definition it controls is always current.
1461
- *
1462
- * That holds only for views CodeGen writes. A view CodeGen does not own — such as the
1463
- * application-owned outer view of a layered entity — has no mechanism here to re-resolve it, and
1464
- * `generateViewRefreshSQL` returning empty is a genuine no-op rather than a cheap one. This is
1465
- * why {@link generateBaseView} refuses layered entities outright on PostgreSQL.
1752
+ * PostgreSQL has no `sp_refreshview`. Generated views are re-issued with
1753
+ * `CREATE OR REPLACE` (or the 42P16 capture/DROP CASCADE path) every run, so
1754
+ * they do not need a separate refresh step. Layered *outer* views are rebound
1755
+ * via {@link generateLayeredOuterRebindSQL}, not this flag.
1466
1756
  */
1467
1757
  get NeedsViewRefresh() {
1468
1758
  return false;
@@ -1471,12 +1761,77 @@ ORDER BY ordinal_position`;
1471
1761
  generateViewRefreshSQL(_schema, _viewName) {
1472
1762
  return '';
1473
1763
  }
1764
+ /**
1765
+ * Restar `g.*` on the application-owned outer view. Invokes the catalog
1766
+ * function shipped by the PG layered-views migration; no-op if the function
1767
+ * is not installed yet (bootstrap before that migration).
1768
+ */
1769
+ generateLayeredOuterRebindSQL(entity) {
1770
+ if (!entity.HasLayeredBaseView) {
1771
+ return '';
1772
+ }
1773
+ let core = '__mj';
1774
+ try {
1775
+ core = (mj_core_schema() || '__mj').replace(/"/g, '""');
1776
+ }
1777
+ catch {
1778
+ core = '__mj';
1779
+ }
1780
+ const schema = entity.SchemaName.replace(/'/g, "''");
1781
+ const outer = entity.BaseView.replace(/'/g, "''");
1782
+ const inner = entity.GeneratedViewName.replace(/'/g, "''");
1783
+ return `SELECT "${core}"."spRebindLayeredOuterView"('${schema}', '${outer}', '${inner}');`;
1784
+ }
1785
+ /**
1786
+ * After the inner generated view is current, restar the application-owned
1787
+ * outer wrapper so `g.*` re-expands. No-op when the entity is not layered
1788
+ * or the outer view does not exist yet (bootstrap pass).
1789
+ */
1790
+ async rebindLayeredOuterIfPresent(client, entity, willRegenerate) {
1791
+ if (!entity.HasLayeredBaseView) {
1792
+ return;
1793
+ }
1794
+ const schemaLit = entity.SchemaName.replace(/'/g, "''").replace(/"/g, '""');
1795
+ const outerLit = entity.BaseView.replace(/'/g, "''").replace(/"/g, '""');
1796
+ const innerLit = entity.GeneratedViewName.replace(/'/g, "''").replace(/"/g, '""');
1797
+ const existsResult = await client.query(`SELECT to_regclass('"${schemaLit}"."${outerLit}"') IS NOT NULL AS present`);
1798
+ if (!existsResult.rows?.[0]?.['present']) {
1799
+ return;
1800
+ }
1801
+ const defResult = await client.query(this.getViewDefinitionSQL(entity.SchemaName, entity.BaseView));
1802
+ const viewDefinition = String(defResult.rows?.[0]?.['ViewDefinition'] ?? defResult.rows?.[0]?.['viewdefinition'] ?? '');
1803
+ if (!viewDefinition) {
1804
+ throw new LayeredOuterRestarError(`Could not read pg_get_viewdef for layered outer view ${entity.SchemaName}.${entity.BaseView}`);
1805
+ }
1806
+ const colResult = await client.query(`SELECT a.attname AS "Name"
1807
+ FROM pg_attribute a
1808
+ JOIN pg_class c ON c.oid = a.attrelid
1809
+ JOIN pg_namespace n ON n.oid = c.relnamespace
1810
+ WHERE n.nspname = '${schemaLit}'
1811
+ AND c.relname = '${innerLit}'
1812
+ AND a.attnum > 0
1813
+ AND NOT a.attisdropped
1814
+ ORDER BY a.attnum`);
1815
+ const innerColumns = (colResult.rows ?? []).map((r) => String(r['Name'] ?? r['name'] ?? ''));
1816
+ const restarred = restarLayeredOuterView({
1817
+ viewDefinition,
1818
+ innerViewName: entity.GeneratedViewName,
1819
+ innerColumns,
1820
+ });
1821
+ const createSQL = buildCreateOrReplaceLayeredOuterViewSQL(entity.SchemaName, entity.BaseView, restarred);
1822
+ await executeWithFallback({
1823
+ client,
1824
+ schema: entity.SchemaName,
1825
+ viewName: entity.BaseView,
1826
+ createOrReplaceSQL: createSQL,
1827
+ willRegenerate,
1828
+ });
1829
+ }
1474
1830
  /** @inheritdoc */
1475
1831
  generateIfViewExistsSQL(schema, viewName, innerSQL) {
1476
- // Reached only if a future change enables layering on PG; today generateBaseView throws
1477
- // first. Implemented properly regardless, so the guard is not a lie if that day comes.
1832
+ // The outer view of a layered entity does not exist on the bootstrap pass.
1478
1833
  const escaped = innerSQL.replace(/'/g, "''");
1479
- const regclass = `${schema}.${viewName}`.replace(/'/g, "''");
1834
+ const regclass = `"${schema.replace(/"/g, '""')}"."${viewName.replace(/"/g, '""')}"`.replace(/'/g, "''");
1480
1835
  return `DO $if_view_exists$
1481
1836
  BEGIN
1482
1837
  IF to_regclass('${regclass}') IS NOT NULL THEN
@@ -1499,107 +1854,18 @@ $if_view_exists$;
1499
1854
  return true;
1500
1855
  }
1501
1856
  // ─── METADATA MANAGEMENT: SQL QUOTING ────────────────────────────
1502
- /**
1503
- * SQL keywords that should NOT be quoted even when they match PascalCase patterns.
1504
- */
1505
- static { this._SQL_KEYWORDS = new Set([
1506
- // DML/DDL keywords
1507
- 'SELECT', 'INSERT', 'INTO', 'UPDATE', 'DELETE', 'FROM', 'WHERE', 'AND', 'OR', 'NOT',
1508
- 'JOIN', 'LEFT', 'RIGHT', 'INNER', 'OUTER', 'CROSS', 'FULL', 'ON', 'AS', 'SET',
1509
- 'VALUES', 'NULL', 'LIKE', 'IN', 'EXISTS', 'BETWEEN', 'CASE', 'WHEN', 'THEN',
1510
- 'ELSE', 'END', 'ORDER', 'BY', 'GROUP', 'HAVING', 'LIMIT', 'OFFSET', 'UNION',
1511
- 'ALL', 'CREATE', 'ALTER', 'DROP', 'TABLE', 'INDEX', 'VIEW', 'EXEC', 'DECLARE',
1512
- 'BEGIN', 'COMMIT', 'ROLLBACK', 'TRANSACTION', 'TRUE', 'FALSE', 'IS', 'ASC', 'DESC',
1513
- 'DISTINCT', 'PRIMARY', 'KEY', 'FOREIGN', 'REFERENCES', 'CONSTRAINT', 'DEFAULT',
1514
- 'IF', 'OBJECT', 'TOP', 'WITH', 'OVER', 'PARTITION', 'ROW_NUMBER', 'RANK',
1515
- 'DENSE_RANK', 'LAG', 'LEAD', 'FIRST_VALUE', 'LAST_VALUE', 'ROWS', 'RANGE',
1516
- 'PRECEDING', 'FOLLOWING', 'UNBOUNDED', 'CURRENT', 'ROW', 'FETCH', 'NEXT', 'ONLY',
1517
- 'SCHEMA', 'CASCADE', 'RESTRICT', 'NO', 'ACTION', 'TRIGGER', 'FUNCTION', 'PROCEDURE',
1518
- 'RETURNS', 'RETURN', 'EXECUTE', 'CALL', 'RAISE', 'NOTICE', 'EXCEPTION', 'PERFORM',
1519
- 'GRANT', 'REVOKE', 'TO', 'USAGE', 'PRIVILEGES', 'OWNER',
1520
- // DDL sub-keywords
1521
- 'ADD', 'COLUMN', 'DO', 'RENAME', 'COMMENT', 'UNIQUE', 'CHECK',
1522
- 'CONFLICT', 'NOTHING', 'EXCLUDED', 'ZONE', 'AT', 'FOR', 'EACH', 'OF',
1523
- 'BEFORE', 'AFTER', 'INSTEAD', 'USING', 'ANY', 'SOME',
1524
- 'ENABLE', 'DISABLE', 'GENERATED', 'ALWAYS', 'IDENTITY',
1525
- 'SECURITY', 'DEFINER', 'INVOKER', 'FORCE', 'COPY',
1526
- 'TEMPORARY', 'TEMP', 'RECURSIVE', 'MATERIALIZED', 'CONCURRENTLY',
1527
- // PL/pgSQL control flow
1528
- 'NEW', 'OLD', 'FOUND', 'LOOP', 'WHILE', 'EXIT', 'CONTINUE',
1529
- 'ELSIF', 'ELSEIF', 'STRICT',
1530
- // Transaction / constraint control (used by SET CONSTRAINTS ALL IMMEDIATE
1531
- // emitted before ALTER TABLE so deferred trigger events flush). Without
1532
- // CONSTRAINTS / IMMEDIATE / DEFERRED in the keyword set, the tokenizer
1533
- // double-quotes them as identifiers and PG rejects the resulting SQL.
1534
- 'CONSTRAINTS', 'IMMEDIATE', 'DEFERRED', 'SAVEPOINT', 'RELEASE',
1535
- // SQL Server types
1536
- 'NVARCHAR', 'VARCHAR', 'UNIQUEIDENTIFIER', 'DATETIMEOFFSET', 'DATETIME', 'DATETIME2',
1537
- 'BIGINT', 'SMALLINT', 'TINYINT', 'FLOAT', 'REAL', 'DECIMAL', 'NUMERIC', 'MONEY',
1538
- 'BIT', 'INT', 'TEXT', 'NTEXT', 'IMAGE', 'BINARY', 'VARBINARY', 'CHAR', 'NCHAR',
1539
- 'XML', 'GEOGRAPHY', 'GEOMETRY', 'HIERARCHYID', 'SQL_VARIANT', 'SYSNAME',
1540
- 'NEWSEQUENTIALID', 'NEWID', 'GETUTCDATE', 'GETDATE', 'SYSDATETIMEOFFSET',
1541
- 'OBJECT_ID', 'SCOPE_IDENTITY',
1542
- // Aggregate / scalar functions
1543
- 'COUNT', 'MAX', 'MIN', 'SUM', 'AVG', 'COALESCE', 'CAST', 'CONVERT', 'ISNULL',
1544
- 'LEN', 'LENGTH', 'DATALENGTH', 'LOWER', 'UPPER', 'LTRIM', 'RTRIM', 'TRIM', 'REPLACE',
1545
- 'SUBSTRING', 'CHARINDEX', 'PATINDEX', 'STUFF', 'CONCAT', 'FORMAT',
1546
- 'LEFT', 'RIGHT', 'POSITION', 'OVERLAY', 'EXTRACT', 'GREATEST', 'LEAST',
1547
- 'DATEADD', 'DATEDIFF', 'DATEPART', 'YEAR', 'MONTH', 'DAY', 'HOUR', 'MINUTE',
1548
- 'SECOND', 'NOW', 'CURRENT_TIMESTAMP',
1549
- // PostgreSQL specific
1550
- 'BOOLEAN', 'SERIAL', 'BIGSERIAL', 'UUID', 'JSONB', 'JSON', 'ARRAY', 'TIMESTAMPTZ',
1551
- 'TIMESTAMP', 'DATE', 'TIME', 'INTERVAL', 'CITEXT', 'INET', 'MACADDR',
1552
- 'GEN_RANDOM_UUID', 'TO_CHAR', 'TO_DATE', 'TO_TIMESTAMP', 'TO_NUMBER',
1553
- 'STRING_AGG', 'ARRAY_AGG', 'UNNEST', 'LATERAL', 'ILIKE',
1554
- 'LANGUAGE', 'PLPGSQL', 'VOLATILE', 'STABLE', 'IMMUTABLE', 'SETOF', 'RECORD',
1555
- 'INOUT', 'OUT', 'VARIADIC', 'PARALLEL', 'SAFE', 'UNSAFE',
1556
- // information_schema column names
1557
- 'TABLE_SCHEMA', 'TABLE_NAME', 'TABLE_CATALOG', 'COLUMN_NAME', 'DATA_TYPE',
1558
- 'IS_NULLABLE', 'COLUMN_DEFAULT', 'CHARACTER_MAXIMUM_LENGTH', 'NUMERIC_PRECISION',
1559
- 'NUMERIC_SCALE', 'ORDINAL_POSITION', 'COLUMN_COMMENT',
1560
- // MJ SQL constructs
1561
- 'INFORMATION_SCHEMA', 'COLUMNS', 'TABLES', 'ROUTINES',
1562
- ]); }
1563
1857
  /**
1564
1858
  * Quotes mixed-case identifiers in a SQL string for PostgreSQL compatibility.
1565
- * Uses a tokenizer approach to skip string literals, already-quoted identifiers,
1566
- * dollar-quoted blocks, and SQL keywords. Any remaining PascalCase word gets
1567
- * double-quoted to preserve case.
1859
+ *
1860
+ * The tokenizer lives in `@memberjunction/sql-dialect` and is shared with
1861
+ * `PostgreSQLDataProvider.autoQuoteIdentifiers`, so codegen-time and runtime SQL are
1862
+ * quoted by one implementation rather than two hand-synced copies. See
1863
+ * {@link AutoQuotePostgreSQLIdentifiers} for the quoting rule and its rationale.
1864
+ *
1865
+ * @inheritdoc
1568
1866
  */
1569
1867
  quoteSQLForExecution(sql) {
1570
- const result = [];
1571
- let i = 0;
1572
- const len = sql.length;
1573
- while (i < len) {
1574
- const ch = sql[i];
1575
- if (ch === "'") {
1576
- i = this.skipSingleQuotedString(sql, i, len, result);
1577
- continue;
1578
- }
1579
- if (ch === '$') {
1580
- i = this.skipDollarQuotedBlock(sql, i, len, result);
1581
- continue;
1582
- }
1583
- if (ch === '"') {
1584
- i = this.skipDoubleQuotedIdentifier(sql, i, len, result);
1585
- continue;
1586
- }
1587
- if (ch === '[') {
1588
- i = this.skipBracketedIdentifier(sql, i, len, result);
1589
- continue;
1590
- }
1591
- if (ch === '@') {
1592
- i = this.skipAtParameter(sql, i, len, result);
1593
- continue;
1594
- }
1595
- if (/[a-zA-Z_]/.test(ch)) {
1596
- i = this.processWord(sql, i, len, result);
1597
- continue;
1598
- }
1599
- result.push(ch);
1600
- i++;
1601
- }
1602
- return result.join('');
1868
+ return AutoQuotePostgreSQLIdentifiers(sql);
1603
1869
  }
1604
1870
  // ─── METADATA MANAGEMENT: DEFAULT VALUE PARSING ──────────────────
1605
1871
  /**
@@ -1757,6 +2023,18 @@ WHERE p.prokind IN ('f', 'p')
1757
2023
  // in the dialect-agnostic recovery loop.
1758
2024
  const recursiveFKs = entity.Fields.filter(f => f.RelatedEntityID != null && UUIDsEqual(f.RelatedEntityID, entity.ID));
1759
2025
  for (const field of recursiveFKs) {
2026
+ const metaSQL = this.generateHierarchyMetaFunction(entity, field);
2027
+ if (metaSQL && metaSQL.trim().length > 0) {
2028
+ await client.query(metaSQL);
2029
+ }
2030
+ const descSQL = this.generateDescendantsFunction(entity, field);
2031
+ if (descSQL && descSQL.trim().length > 0) {
2032
+ await client.query(descSQL);
2033
+ }
2034
+ const ancSQL = this.generateAncestorsFunction(entity, field);
2035
+ if (ancSQL && ancSQL.trim().length > 0) {
2036
+ await client.query(ancSQL);
2037
+ }
1760
2038
  const fnSQL = this.generateRootIDFunction(entity, field);
1761
2039
  if (fnSQL && fnSQL.trim().length > 0) {
1762
2040
  await client.query(fnSQL);
@@ -1765,7 +2043,7 @@ WHERE p.prokind IN ('f', 'p')
1765
2043
  await executeWithFallback({
1766
2044
  client,
1767
2045
  schema: entity.SchemaName,
1768
- viewName: entity.BaseView,
2046
+ viewName: entity.GeneratedViewName,
1769
2047
  createOrReplaceSQL: viewSQL,
1770
2048
  willRegenerate,
1771
2049
  // Pass the base table so viewFallback can materialize a stub
@@ -1776,6 +2054,7 @@ WHERE p.prokind IN ('f', 'p')
1776
2054
  // self-reference until a placeholder exists).
1777
2055
  baseTableQualified: pgDialect.QuoteSchema(entity.SchemaName, entity.BaseTable),
1778
2056
  });
2057
+ await this.rebindLayeredOuterIfPresent(client, entity, willRegenerate);
1779
2058
  }
1780
2059
  finally {
1781
2060
  client.release();
@@ -1822,11 +2101,12 @@ WHERE p.prokind IN ('f', 'p')
1822
2101
  await executeWithFallback({
1823
2102
  client,
1824
2103
  schema: opts.entity.SchemaName,
1825
- viewName: opts.entity.BaseView,
2104
+ viewName: opts.entity.GeneratedViewName,
1826
2105
  createOrReplaceSQL: opts.viewSQL,
1827
2106
  willRegenerate: opts.willRegenerate,
1828
2107
  baseTableQualified: pgDialect.QuoteSchema(opts.entity.SchemaName, opts.entity.BaseTable),
1829
2108
  });
2109
+ await this.rebindLayeredOuterIfPresent(client, opts.entity, opts.willRegenerate);
1830
2110
  }
1831
2111
  catch (e) {
1832
2112
  return {
@@ -1947,38 +2227,6 @@ WHERE p.prokind IN ('f', 'p')
1947
2227
  getBaseViewName(entity) {
1948
2228
  return entity.BaseView || `vw_${this.toSnakeCase(entity.CodeName)}`;
1949
2229
  }
1950
- /**
1951
- * Refuses layered base views on PostgreSQL, where the arrangement cannot deliver what it
1952
- * promises.
1953
- *
1954
- * Layering exists so an application can add a computed column without inheriting — and then
1955
- * hand-maintaining — the generated view, the payoff being that a foreign key added later still
1956
- * shows up on its own. That payoff depends entirely on the application-owned outer view's
1957
- * `SELECT g.*` being re-resolved after the inner view regenerates. SQL Server does that with
1958
- * `sp_refreshview`. PostgreSQL expands `*` at creation and freezes it, offers no refresh
1959
- * equivalent, and CodeGen does not own the outer view, so nothing recreates it.
1960
- *
1961
- * The resulting behaviour is worse than plainly broken, it is intermittent: an ADDED column (the
1962
- * common case) leaves the outer view stale, because `CREATE OR REPLACE` on the inner view
1963
- * succeeds and never touches dependents. A column RENAME or type change raises 42P16, which
1964
- * sends CodeGen down the capture/`DROP CASCADE`/replay path — and that incidentally recreates
1965
- * the outer view, so it picks the new columns up. Same feature, opposite outcomes, decided by
1966
- * which kind of schema change happened to land that day.
1967
- *
1968
- * That is precisely the silent-staleness failure layering was built to eliminate, so this throws
1969
- * rather than documenting a footgun. Fully custom base views (`BaseViewGenerated = 0` with no
1970
- * `GeneratedBaseViewName`) are unaffected and keep working on PostgreSQL as before.
1971
- */
1972
- assertLayeredBaseViewSupported(entity) {
1973
- if (!entity.HasLayeredBaseView)
1974
- return;
1975
- throw new Error(`Entity "${entity.Name}" sets GeneratedBaseViewName = '${entity.GeneratedBaseViewName}', but layered ` +
1976
- `base views are not supported on PostgreSQL. PostgreSQL freezes a view's column list at creation and ` +
1977
- `has no sp_refreshview equivalent, so the application-owned view "${entity.BaseView}" would silently ` +
1978
- `stop gaining columns that the generated view underneath it picks up. Clear GeneratedBaseViewName and ` +
1979
- `use a fully custom base view (BaseViewGenerated = 0) instead, accepting that it must be ` +
1980
- `hand-maintained as the schema changes.`);
1981
- }
1982
2230
  /** Builds the WHERE clause for soft-delete filtering */
1983
2231
  buildSoftDeleteWhereClause(entity, alias) {
1984
2232
  if (entity.DeleteType === 'Soft') {
@@ -2232,102 +2480,6 @@ WHERE p.prokind IN ('f', 'p')
2232
2480
  AND tablename = '${tableName}'
2233
2481
  AND indexname = '${indexName}'`;
2234
2482
  }
2235
- // ─── TOKENIZER HELPERS (for quoteSQLForExecution) ────────────────
2236
- /** Skips a single-quoted string literal, handling escaped quotes ('') */
2237
- skipSingleQuotedString(sql, start, len, result) {
2238
- let j = start + 1;
2239
- while (j < len) {
2240
- if (sql[j] === "'" && j + 1 < len && sql[j + 1] === "'") {
2241
- j += 2;
2242
- }
2243
- else if (sql[j] === "'") {
2244
- j++;
2245
- break;
2246
- }
2247
- else {
2248
- j++;
2249
- }
2250
- }
2251
- result.push(sql.substring(start, j));
2252
- return j;
2253
- }
2254
- /** Skips a dollar-quoted block ($$ ... $$ or $tag$ ... $tag$) */
2255
- skipDollarQuotedBlock(sql, start, len, result) {
2256
- let tagEnd = start + 1;
2257
- if (tagEnd < len && sql[tagEnd] === '$') {
2258
- // Simple $$ tag
2259
- tagEnd = start + 2;
2260
- }
2261
- else {
2262
- // Look for $identifier$ pattern
2263
- while (tagEnd < len && /[a-zA-Z0-9_]/.test(sql[tagEnd]))
2264
- tagEnd++;
2265
- if (tagEnd < len && sql[tagEnd] === '$') {
2266
- tagEnd++;
2267
- }
2268
- else {
2269
- // Not a dollar-quote, just a $ character
2270
- result.push(sql[start]);
2271
- return start + 1;
2272
- }
2273
- }
2274
- const tag = sql.substring(start, tagEnd);
2275
- const closePos = sql.indexOf(tag, tagEnd);
2276
- if (closePos !== -1) {
2277
- const blockEnd = closePos + tag.length;
2278
- result.push(sql.substring(start, blockEnd));
2279
- return blockEnd;
2280
- }
2281
- // No closing tag found, pass through rest of string
2282
- result.push(sql.substring(start));
2283
- return len;
2284
- }
2285
- /** Skips an already double-quoted identifier */
2286
- skipDoubleQuotedIdentifier(sql, start, len, result) {
2287
- let j = start + 1;
2288
- while (j < len && sql[j] !== '"')
2289
- j++;
2290
- if (j < len)
2291
- j++;
2292
- result.push(sql.substring(start, j));
2293
- return j;
2294
- }
2295
- /** Skips a square-bracketed identifier (SQL Server style) */
2296
- skipBracketedIdentifier(sql, start, len, result) {
2297
- let j = start + 1;
2298
- while (j < len && sql[j] !== ']')
2299
- j++;
2300
- if (j < len)
2301
- j++;
2302
- result.push(sql.substring(start, j));
2303
- return j;
2304
- }
2305
- /** Skips an @-prefixed parameter */
2306
- skipAtParameter(sql, start, len, result) {
2307
- let j = start + 1;
2308
- while (j < len && /[a-zA-Z0-9_]/.test(sql[j]))
2309
- j++;
2310
- result.push(sql.substring(start, j));
2311
- return j;
2312
- }
2313
- /** Processes a word token - quotes it if it's a PascalCase identifier, not a keyword */
2314
- processWord(sql, start, len, result) {
2315
- let j = start + 1;
2316
- while (j < len && /[a-zA-Z0-9_]/.test(sql[j]))
2317
- j++;
2318
- const word = sql.substring(start, j);
2319
- const isKeyword = PostgreSQLCodeGenProvider_1._SQL_KEYWORDS.has(word.toUpperCase());
2320
- const startsUpper = /^[A-Z]/.test(word);
2321
- const isAllLower = word === word.toLowerCase();
2322
- const isMJInternal = word.startsWith('__mj_');
2323
- if (!isKeyword && !isAllLower && !isMJInternal && startsUpper) {
2324
- result.push(pgDialect.QuoteIdentifier(word));
2325
- }
2326
- else {
2327
- result.push(word);
2328
- }
2329
- return j;
2330
- }
2331
2483
  // ─── COMPLEX SQL GENERATION HELPERS ──────────────────────────────
2332
2484
  /**
2333
2485
  * Builds the full pending entity fields query for PostgreSQL.
@@ -2461,7 +2613,7 @@ WHERE vf."IsVirtual" = true
2461
2613
  `;
2462
2614
  }
2463
2615
  };
2464
- PostgreSQLCodeGenProvider = PostgreSQLCodeGenProvider_1 = __decorate([
2616
+ PostgreSQLCodeGenProvider = __decorate([
2465
2617
  RegisterClass(CodeGenDatabaseProvider, 'postgresql')
2466
2618
  ], PostgreSQLCodeGenProvider);
2467
2619
  export { PostgreSQLCodeGenProvider };