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

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 (94) hide show
  1. package/LICENSE +180 -4
  2. package/README.md +39 -22
  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/Database/codeGenDatabaseProvider.d.ts +101 -0
  12. package/dist/Database/codeGenDatabaseProvider.d.ts.map +1 -1
  13. package/dist/Database/codeGenDatabaseProvider.js +77 -10
  14. package/dist/Database/codeGenDatabaseProvider.js.map +1 -1
  15. package/dist/Database/manage-metadata.d.ts +292 -2
  16. package/dist/Database/manage-metadata.d.ts.map +1 -1
  17. package/dist/Database/manage-metadata.js +1176 -45
  18. package/dist/Database/manage-metadata.js.map +1 -1
  19. package/dist/Database/materializationAnalysis.d.ts +241 -0
  20. package/dist/Database/materializationAnalysis.d.ts.map +1 -0
  21. package/dist/Database/materializationAnalysis.js +415 -0
  22. package/dist/Database/materializationAnalysis.js.map +1 -0
  23. package/dist/Database/materializationBroadRender.d.ts +67 -0
  24. package/dist/Database/materializationBroadRender.d.ts.map +1 -0
  25. package/dist/Database/materializationBroadRender.js +104 -0
  26. package/dist/Database/materializationBroadRender.js.map +1 -0
  27. package/dist/Database/materializationDrift.d.ts +59 -0
  28. package/dist/Database/materializationDrift.d.ts.map +1 -0
  29. package/dist/Database/materializationDrift.js +85 -0
  30. package/dist/Database/materializationDrift.js.map +1 -0
  31. package/dist/Database/materializationParamClassifier.d.ts +92 -0
  32. package/dist/Database/materializationParamClassifier.d.ts.map +1 -0
  33. package/dist/Database/materializationParamClassifier.js +224 -0
  34. package/dist/Database/materializationParamClassifier.js.map +1 -0
  35. package/dist/Database/materializationParamVerifier.d.ts +71 -0
  36. package/dist/Database/materializationParamVerifier.d.ts.map +1 -0
  37. package/dist/Database/materializationParamVerifier.js +302 -0
  38. package/dist/Database/materializationParamVerifier.js.map +1 -0
  39. package/dist/Database/materializationSqlAst.d.ts +78 -0
  40. package/dist/Database/materializationSqlAst.d.ts.map +1 -0
  41. package/dist/Database/materializationSqlAst.js +142 -0
  42. package/dist/Database/materializationSqlAst.js.map +1 -0
  43. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts +68 -21
  44. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts.map +1 -1
  45. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js +303 -197
  46. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js.map +1 -1
  47. package/dist/Database/providers/postgresql/__tests__/PostgreSQLCodeGenProvider.materialization.test.d.ts +2 -0
  48. package/dist/Database/providers/postgresql/__tests__/PostgreSQLCodeGenProvider.materialization.test.d.ts.map +1 -0
  49. package/dist/Database/providers/postgresql/__tests__/PostgreSQLCodeGenProvider.materialization.test.js +119 -0
  50. package/dist/Database/providers/postgresql/__tests__/PostgreSQLCodeGenProvider.materialization.test.js.map +1 -0
  51. package/dist/Database/providers/postgresql/__tests__/PostgreSQLHierarchyFunctions.test.d.ts +2 -0
  52. package/dist/Database/providers/postgresql/__tests__/PostgreSQLHierarchyFunctions.test.d.ts.map +1 -0
  53. package/dist/Database/providers/postgresql/__tests__/PostgreSQLHierarchyFunctions.test.js +155 -0
  54. package/dist/Database/providers/postgresql/__tests__/PostgreSQLHierarchyFunctions.test.js.map +1 -0
  55. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts +60 -1
  56. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts.map +1 -1
  57. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js +294 -0
  58. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js.map +1 -1
  59. package/dist/Database/providers/sqlserver/__tests__/BaseViewEmissionGoldenMaster.test.d.ts +2 -0
  60. package/dist/Database/providers/sqlserver/__tests__/BaseViewEmissionGoldenMaster.test.d.ts.map +1 -0
  61. package/dist/Database/providers/sqlserver/__tests__/BaseViewEmissionGoldenMaster.test.js +547 -0
  62. package/dist/Database/providers/sqlserver/__tests__/BaseViewEmissionGoldenMaster.test.js.map +1 -0
  63. package/dist/Database/providers/sqlserver/__tests__/BaseViewRegenDecision.test.d.ts +2 -0
  64. package/dist/Database/providers/sqlserver/__tests__/BaseViewRegenDecision.test.d.ts.map +1 -0
  65. package/dist/Database/providers/sqlserver/__tests__/BaseViewRegenDecision.test.js +721 -0
  66. package/dist/Database/providers/sqlserver/__tests__/BaseViewRegenDecision.test.js.map +1 -0
  67. package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.materialization.test.d.ts +2 -0
  68. package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.materialization.test.d.ts.map +1 -0
  69. package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.materialization.test.js +78 -0
  70. package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.materialization.test.js.map +1 -0
  71. package/dist/Database/providers/sqlserver/__tests__/SQLServerHierarchyTVF.test.d.ts +2 -0
  72. package/dist/Database/providers/sqlserver/__tests__/SQLServerHierarchyTVF.test.d.ts.map +1 -0
  73. package/dist/Database/providers/sqlserver/__tests__/SQLServerHierarchyTVF.test.js +155 -0
  74. package/dist/Database/providers/sqlserver/__tests__/SQLServerHierarchyTVF.test.js.map +1 -0
  75. package/dist/Database/sql_codegen.d.ts +21 -5
  76. package/dist/Database/sql_codegen.d.ts.map +1 -1
  77. package/dist/Database/sql_codegen.js +84 -26
  78. package/dist/Database/sql_codegen.js.map +1 -1
  79. package/dist/Misc/action_subclasses_codegen.d.ts.map +1 -1
  80. package/dist/Misc/action_subclasses_codegen.js +4 -4
  81. package/dist/Misc/action_subclasses_codegen.js.map +1 -1
  82. package/dist/Misc/entity_subclasses_codegen.d.ts +44 -0
  83. package/dist/Misc/entity_subclasses_codegen.d.ts.map +1 -1
  84. package/dist/Misc/entity_subclasses_codegen.js +369 -156
  85. package/dist/Misc/entity_subclasses_codegen.js.map +1 -1
  86. package/dist/Misc/graphql_server_codegen.d.ts +4 -58
  87. package/dist/Misc/graphql_server_codegen.d.ts.map +1 -1
  88. package/dist/Misc/graphql_server_codegen.js +22 -236
  89. package/dist/Misc/graphql_server_codegen.js.map +1 -1
  90. package/dist/Misc/sql_logging.d.ts +25 -0
  91. package/dist/Misc/sql_logging.d.ts.map +1 -1
  92. package/dist/Misc/sql_logging.js +51 -8
  93. package/dist/Misc/sql_logging.js.map +1 -1
  94. 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 } 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;
@@ -382,6 +380,63 @@ EXCEPTION WHEN invalid_table_definition THEN
382
380
  END $vw_regen$;
383
381
  `;
384
382
  }
383
+ // ─── MATERIALIZATION ─────────────────────────────────────────────────
384
+ /**
385
+ * PostgreSQL materialized-table DDL. The create is **conditional** (`CREATE TABLE IF NOT
386
+ * EXISTS`) so a migration-provided `materialized_<name>` table with bespoke indexing is
387
+ * detected and reused rather than clobbered (plan §12). Emits the single-column surrogate
388
+ * PRIMARY KEY, which is itself the required unique index.
389
+ *
390
+ * PG dialect vs. SQL Server: double-quoted identifiers, `CREATE TABLE IF NOT EXISTS` in
391
+ * place of the `IF OBJECT_ID(...) IS NULL` guard, and the surrogate column carries PG's
392
+ * `GENERATED ALWAYS AS IDENTITY` clause (see {@link getMaterializedSurrogateColumnType}).
393
+ */
394
+ generateMaterializedTableSQL(schema, tableName, columns) {
395
+ // MJ entity-field metadata stores CANONICAL (SQL Server-flavored) type names — e.g. 'uniqueidentifier',
396
+ // 'nvarchar(50)' — regardless of the physical DB dialect. Every other PG DDL path maps these via
397
+ // mapSQLType (see spCreate/spUpdate generation); the materialized-table DDL must do the same, or a
398
+ // base-view/query materialization emits `... uniqueidentifier ...` and PG fails the CREATE TABLE with
399
+ // `type "uniqueidentifier" does not exist`, aborting the whole codegen run.
400
+ const colLines = columns.map((c) => ` ${pgDialect.QuoteIdentifier(c.Name)} ${this.mapSQLType(c.SQLType)} ${c.Nullable ? 'NULL' : 'NOT NULL'}`);
401
+ const pkCols = columns.filter((c) => c.IsPrimaryKey).map((c) => c.Name);
402
+ const pkClause = pkCols.length
403
+ ? `,\n CONSTRAINT ${pgDialect.QuoteIdentifier(`PK_${tableName}`)} PRIMARY KEY (${pkCols
404
+ .map((n) => pgDialect.QuoteIdentifier(n))
405
+ .join(', ')})`
406
+ : '';
407
+ return `CREATE TABLE IF NOT EXISTS ${pgDialect.QuoteSchema(schema, tableName)} (
408
+ ${colLines.join(',\n')}${pkClause}
409
+ );`;
410
+ }
411
+ /**
412
+ * PostgreSQL wrapper-view DDL — the stable read contract over the materialized table.
413
+ * Uses `CREATE OR REPLACE VIEW` so the same statement both creates the view and atomically
414
+ * repoints it at a freshly-built table during refresh (plan §11.2). The body is always
415
+ * `SELECT * FROM <table>` and the shadow table shares the column shape, so PG's
416
+ * `CREATE OR REPLACE` column-compatibility rule is satisfied on the swap.
417
+ */
418
+ generateMaterializedWrapperViewSQL(schema, viewName, tableName) {
419
+ return `CREATE OR REPLACE VIEW ${pgDialect.QuoteSchema(schema, viewName)}
420
+ AS
421
+ SELECT * FROM ${pgDialect.QuoteSchema(schema, tableName)};`;
422
+ }
423
+ /**
424
+ * PostgreSQL synthetic surrogate key: a SQL-standard auto-assigned identity column
425
+ * (`GENERATED ALWAYS AS IDENTITY`), `bigint` for headroom on large materialized sets.
426
+ * v1 full-rebuild only; the deterministic combined-key hashing in §5 replaces this in Phase 3.
427
+ */
428
+ getMaterializedSurrogateColumnType() {
429
+ return 'bigint GENERATED ALWAYS AS IDENTITY';
430
+ }
431
+ /**
432
+ * PostgreSQL KEYED surrogate key type: the combined-key SHA-256 hash the refresh writes comes from
433
+ * `encode(digest(…), 'hex')`, whose type is `text`, and the full rebuild's `CREATE TABLE AS` infers
434
+ * exactly that. Typing the minted entity's PK column `text` MATCHES the post-refresh physical column,
435
+ * avoiding the int-vs-hash divergence between CodeGen metadata and the rebuilt table.
436
+ */
437
+ getMaterializedHashSurrogateColumnType() {
438
+ return 'text';
439
+ }
385
440
  // ─── CRUD CREATE ─────────────────────────────────────────────────────
386
441
  /**
387
442
  * Emits a `DO` block that drops every existing overload of `<schema>.<fnName>`.
@@ -955,7 +1010,231 @@ WHERE ${ftsColName} IS NULL;
955
1010
  * the parent FK upward, capped at 100 levels to prevent infinite loops. Returns the
956
1011
  * root record's primary key value.
957
1012
  */
1013
+ // ─── RECURSIVE HIERARCHY FUNCTIONS ──────────────────────────────────
1014
+ /**
1015
+ * Generates a PostgreSQL function returning a table of hierarchy metadata
1016
+ * (RootID, Depth, Path, IsLeaf, ChildCount) for a self-referencing foreign key.
1017
+ */
1018
+ generateHierarchyMetaFunction(entity, field) {
1019
+ if (entity.PrimaryKeys.length !== 1) {
1020
+ throw new Error(`[Hierarchy] Entity '${entity.Name}' has ${entity.PrimaryKeys.length} primary key fields. MemberJunction hierarchy TVF generation requires a single-column primary key.`);
1021
+ }
1022
+ const primaryKey = entity.FirstPrimaryKey.Name;
1023
+ const primaryKeyType = this.mapSQLType(entity.FirstPrimaryKey.SQLFullType);
1024
+ const fieldName = field.Name;
1025
+ const fnName = this.getHierarchyMetaFunctionName(entity, field);
1026
+ return `
1027
+ ------------------------------------------------------------
1028
+ ----- HIERARCHY METADATA FUNCTION FOR: ${entity.BaseTable}.${fieldName}
1029
+ ------------------------------------------------------------
1030
+ CREATE OR REPLACE FUNCTION ${pgDialect.QuoteSchema(entity.SchemaName, fnName)}(
1031
+ p_record_id ${primaryKeyType},
1032
+ p_parent_id ${primaryKeyType}
1033
+ ) RETURNS TABLE (
1034
+ "RootID" ${primaryKeyType},
1035
+ "Depth" INTEGER,
1036
+ "Path" TEXT,
1037
+ "IsLeaf" BOOLEAN,
1038
+ "ChildCount" INTEGER
1039
+ ) AS $$
1040
+ WITH RECURSIVE cte_ancestors AS (
1041
+ SELECT
1042
+ ${pgDialect.QuoteIdentifier(primaryKey)},
1043
+ ${pgDialect.QuoteIdentifier(fieldName)},
1044
+ 0 AS depth,
1045
+ '/' || ${pgDialect.QuoteIdentifier(primaryKey)}::TEXT || '/' AS path
1046
+ FROM
1047
+ ${pgDialect.QuoteSchema(entity.SchemaName, entity.BaseTable)}
1048
+ WHERE
1049
+ ${pgDialect.QuoteIdentifier(primaryKey)} = p_record_id
1050
+
1051
+ UNION ALL
1052
+
1053
+ SELECT
1054
+ p.${pgDialect.QuoteIdentifier(primaryKey)},
1055
+ p.${pgDialect.QuoteIdentifier(fieldName)},
1056
+ c.depth + 1 AS depth,
1057
+ '/' || p.${pgDialect.QuoteIdentifier(primaryKey)}::TEXT || c.path AS path
1058
+ FROM
1059
+ ${pgDialect.QuoteSchema(entity.SchemaName, entity.BaseTable)} p
1060
+ INNER JOIN
1061
+ cte_ancestors c ON p.${pgDialect.QuoteIdentifier(primaryKey)} = c.${pgDialect.QuoteIdentifier(fieldName)}
1062
+ WHERE
1063
+ c.depth < 100
1064
+ )
1065
+ SELECT
1066
+ a.${pgDialect.QuoteIdentifier(primaryKey)} AS "RootID",
1067
+ (SELECT MAX(depth) FROM cte_ancestors)::INTEGER AS "Depth",
1068
+ (SELECT path FROM cte_ancestors ORDER BY depth DESC LIMIT 1)::TEXT AS "Path",
1069
+ (NOT EXISTS (SELECT 1 FROM ${pgDialect.QuoteSchema(entity.SchemaName, entity.BaseTable)} WHERE ${pgDialect.QuoteIdentifier(fieldName)} = p_record_id))::BOOLEAN AS "IsLeaf",
1070
+ (SELECT COUNT(1)::INTEGER FROM ${pgDialect.QuoteSchema(entity.SchemaName, entity.BaseTable)} WHERE ${pgDialect.QuoteIdentifier(fieldName)} = p_record_id) AS "ChildCount"
1071
+ FROM
1072
+ cte_ancestors a
1073
+ WHERE
1074
+ a.${pgDialect.QuoteIdentifier(fieldName)} IS NULL OR p_parent_id IS NULL
1075
+ ORDER BY
1076
+ a.depth DESC
1077
+ LIMIT 1;
1078
+ $$ LANGUAGE sql STABLE;
1079
+ `;
1080
+ }
1081
+ /**
1082
+ * Generates a PostgreSQL table-valued function returning all descendant records of a root node.
1083
+ */
1084
+ generateDescendantsFunction(entity, field) {
1085
+ if (entity.PrimaryKeys.length !== 1) {
1086
+ throw new Error(`[Hierarchy] Entity '${entity.Name}' has ${entity.PrimaryKeys.length} primary key fields. MemberJunction hierarchy TVF generation requires a single-column primary key.`);
1087
+ }
1088
+ const primaryKey = entity.FirstPrimaryKey.Name;
1089
+ const primaryKeyType = this.mapSQLType(entity.FirstPrimaryKey.SQLFullType);
1090
+ const fieldName = field.Name;
1091
+ const fnName = this.getDescendantsFunctionName(entity, field);
1092
+ return `
1093
+ ------------------------------------------------------------
1094
+ ----- DESCENDANTS FUNCTION FOR: ${entity.BaseTable}.${fieldName}
1095
+ ------------------------------------------------------------
1096
+ CREATE OR REPLACE FUNCTION ${pgDialect.QuoteSchema(entity.SchemaName, fnName)}(
1097
+ p_root_id ${primaryKeyType},
1098
+ p_max_depth INTEGER DEFAULT NULL
1099
+ ) RETURNS TABLE (
1100
+ "ID" ${primaryKeyType},
1101
+ "Depth" INTEGER,
1102
+ "Path" TEXT,
1103
+ "IsLeaf" BOOLEAN,
1104
+ "ChildCount" INTEGER
1105
+ ) AS $$
1106
+ WITH RECURSIVE cte_descendants AS (
1107
+ SELECT
1108
+ ${pgDialect.QuoteIdentifier(primaryKey)},
1109
+ ${pgDialect.QuoteIdentifier(fieldName)},
1110
+ 0 AS relative_depth,
1111
+ '/' || ${pgDialect.QuoteIdentifier(primaryKey)}::TEXT || '/' AS path
1112
+ FROM
1113
+ ${pgDialect.QuoteSchema(entity.SchemaName, entity.BaseTable)}
1114
+ WHERE
1115
+ ${pgDialect.QuoteIdentifier(primaryKey)} = p_root_id
1116
+
1117
+ UNION ALL
1118
+
1119
+ SELECT
1120
+ c.${pgDialect.QuoteIdentifier(primaryKey)},
1121
+ c.${pgDialect.QuoteIdentifier(fieldName)},
1122
+ p.relative_depth + 1 AS relative_depth,
1123
+ p.path || c.${pgDialect.QuoteIdentifier(primaryKey)}::TEXT || '/' AS path
1124
+ FROM
1125
+ ${pgDialect.QuoteSchema(entity.SchemaName, entity.BaseTable)} c
1126
+ INNER JOIN
1127
+ cte_descendants p ON c.${pgDialect.QuoteIdentifier(fieldName)} = p.${pgDialect.QuoteIdentifier(primaryKey)}
1128
+ WHERE
1129
+ (p_max_depth IS NULL OR p.relative_depth < p_max_depth)
1130
+ AND p.relative_depth < 100
1131
+ )
1132
+ SELECT
1133
+ d.${pgDialect.QuoteIdentifier(primaryKey)} AS "ID",
1134
+ d.relative_depth AS "Depth",
1135
+ d.path AS "Path",
1136
+ (NOT EXISTS (SELECT 1 FROM ${pgDialect.QuoteSchema(entity.SchemaName, entity.BaseTable)} WHERE ${pgDialect.QuoteIdentifier(fieldName)} = d.${pgDialect.QuoteIdentifier(primaryKey)}))::BOOLEAN AS "IsLeaf",
1137
+ (SELECT COUNT(1)::INTEGER FROM ${pgDialect.QuoteSchema(entity.SchemaName, entity.BaseTable)} WHERE ${pgDialect.QuoteIdentifier(fieldName)} = d.${pgDialect.QuoteIdentifier(primaryKey)}) AS "ChildCount"
1138
+ FROM
1139
+ cte_descendants d;
1140
+ $$ LANGUAGE sql STABLE;
1141
+ `;
1142
+ }
1143
+ /**
1144
+ * Generates a PostgreSQL table-valued function returning all ancestors of a node walking upward.
1145
+ */
1146
+ generateAncestorsFunction(entity, field) {
1147
+ if (entity.PrimaryKeys.length !== 1) {
1148
+ throw new Error(`[Hierarchy] Entity '${entity.Name}' has ${entity.PrimaryKeys.length} primary key fields. MemberJunction hierarchy TVF generation requires a single-column primary key.`);
1149
+ }
1150
+ const primaryKey = entity.FirstPrimaryKey.Name;
1151
+ const primaryKeyType = this.mapSQLType(entity.FirstPrimaryKey.SQLFullType);
1152
+ const fieldName = field.Name;
1153
+ const fnName = this.getAncestorsFunctionName(entity, field);
1154
+ return `
1155
+ ------------------------------------------------------------
1156
+ ----- ANCESTORS FUNCTION FOR: ${entity.BaseTable}.${fieldName}
1157
+ ------------------------------------------------------------
1158
+ CREATE OR REPLACE FUNCTION ${pgDialect.QuoteSchema(entity.SchemaName, fnName)}(
1159
+ p_record_id ${primaryKeyType}
1160
+ ) RETURNS TABLE (
1161
+ "ID" ${primaryKeyType},
1162
+ "LevelUp" INTEGER,
1163
+ "Path" TEXT
1164
+ ) AS $$
1165
+ WITH RECURSIVE cte_ancestors AS (
1166
+ SELECT
1167
+ ${pgDialect.QuoteIdentifier(primaryKey)},
1168
+ ${pgDialect.QuoteIdentifier(fieldName)},
1169
+ 0 AS level_up,
1170
+ '/' || ${pgDialect.QuoteIdentifier(primaryKey)}::TEXT || '/' AS path
1171
+ FROM
1172
+ ${pgDialect.QuoteSchema(entity.SchemaName, entity.BaseTable)}
1173
+ WHERE
1174
+ ${pgDialect.QuoteIdentifier(primaryKey)} = p_record_id
1175
+
1176
+ UNION ALL
1177
+
1178
+ SELECT
1179
+ p.${pgDialect.QuoteIdentifier(primaryKey)},
1180
+ p.${pgDialect.QuoteIdentifier(fieldName)},
1181
+ c.level_up + 1 AS level_up,
1182
+ '/' || p.${pgDialect.QuoteIdentifier(primaryKey)}::TEXT || c.path AS path
1183
+ FROM
1184
+ ${pgDialect.QuoteSchema(entity.SchemaName, entity.BaseTable)} p
1185
+ INNER JOIN
1186
+ cte_ancestors c ON p.${pgDialect.QuoteIdentifier(primaryKey)} = c.${pgDialect.QuoteIdentifier(fieldName)}
1187
+ WHERE
1188
+ c.level_up < 100
1189
+ )
1190
+ SELECT
1191
+ a.${pgDialect.QuoteIdentifier(primaryKey)} AS "ID",
1192
+ a.level_up AS "LevelUp",
1193
+ a.path AS "Path"
1194
+ FROM
1195
+ cte_ancestors a;
1196
+ $$ LANGUAGE sql STABLE;
1197
+ `;
1198
+ }
1199
+ getHierarchyMetaFunctionName(entity, field) {
1200
+ return `fn_${this.toSnakeCase(entity.BaseTable)}_${this.toSnakeCase(field.Name)}_get_hierarchy_meta`;
1201
+ }
1202
+ getDescendantsFunctionName(entity, field) {
1203
+ return `fn_${this.toSnakeCase(entity.BaseTable)}_${this.toSnakeCase(field.Name)}_get_descendants`;
1204
+ }
1205
+ getAncestorsFunctionName(entity, field) {
1206
+ return `fn_${this.toSnakeCase(entity.BaseTable)}_${this.toSnakeCase(field.Name)}_get_ancestors`;
1207
+ }
1208
+ /** @inheritdoc */
1209
+ generateHierarchyFieldSelect(_entity, field, alias) {
1210
+ const rootFieldName = `Root${field.Name}`;
1211
+ const depthFieldName = `${field.Name}Depth`;
1212
+ const pathFieldName = `${field.Name}Path`;
1213
+ const isLeafFieldName = `${field.Name}IsLeaf`;
1214
+ const childCountFieldName = `${field.Name}ChildCount`;
1215
+ return `${alias}."RootID" AS ${pgDialect.QuoteIdentifier(rootFieldName)},
1216
+ ${alias}."Depth" AS ${pgDialect.QuoteIdentifier(depthFieldName)},
1217
+ ${alias}."Path" AS ${pgDialect.QuoteIdentifier(pathFieldName)},
1218
+ ${alias}."IsLeaf" AS ${pgDialect.QuoteIdentifier(isLeafFieldName)},
1219
+ ${alias}."ChildCount" AS ${pgDialect.QuoteIdentifier(childCountFieldName)}`;
1220
+ }
1221
+ /**
1222
+ * Generates a `LEFT JOIN LATERAL` clause that invokes the hierarchy metadata function
1223
+ * for a self-referencing field.
1224
+ */
1225
+ generateHierarchyFieldJoin(entity, field, alias) {
1226
+ const fnName = this.getHierarchyMetaFunctionName(entity, field);
1227
+ const tableAlias = entity.BaseTableCodeName.charAt(0).toLowerCase();
1228
+ return `LEFT JOIN LATERAL ${pgDialect.QuoteSchema(entity.SchemaName, fnName)}(${tableAlias}.${pgDialect.QuoteIdentifier(entity.FirstPrimaryKey.Name)}, ${tableAlias}.${pgDialect.QuoteIdentifier(field.Name)}) AS ${alias} ON true`;
1229
+ }
1230
+ /**
1231
+ * Generates a PostgreSQL function that recursively calculates the root record ID for a
1232
+ * self-referencing foreign key.
1233
+ */
958
1234
  generateRootIDFunction(entity, field) {
1235
+ if (entity.PrimaryKeys.length !== 1) {
1236
+ throw new Error(`[Hierarchy] Entity '${entity.Name}' has ${entity.PrimaryKeys.length} primary key fields. MemberJunction hierarchy TVF generation requires a single-column primary key.`);
1237
+ }
959
1238
  const primaryKey = entity.FirstPrimaryKey.Name;
960
1239
  const primaryKeyType = this.mapSQLType(entity.FirstPrimaryKey.SQLFullType);
961
1240
  const fieldName = field.Name;
@@ -1499,107 +1778,18 @@ $if_view_exists$;
1499
1778
  return true;
1500
1779
  }
1501
1780
  // ─── 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
1781
  /**
1564
1782
  * 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.
1783
+ *
1784
+ * The tokenizer lives in `@memberjunction/sql-dialect` and is shared with
1785
+ * `PostgreSQLDataProvider.autoQuoteIdentifiers`, so codegen-time and runtime SQL are
1786
+ * quoted by one implementation rather than two hand-synced copies. See
1787
+ * {@link AutoQuotePostgreSQLIdentifiers} for the quoting rule and its rationale.
1788
+ *
1789
+ * @inheritdoc
1568
1790
  */
1569
1791
  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('');
1792
+ return AutoQuotePostgreSQLIdentifiers(sql);
1603
1793
  }
1604
1794
  // ─── METADATA MANAGEMENT: DEFAULT VALUE PARSING ──────────────────
1605
1795
  /**
@@ -1757,6 +1947,18 @@ WHERE p.prokind IN ('f', 'p')
1757
1947
  // in the dialect-agnostic recovery loop.
1758
1948
  const recursiveFKs = entity.Fields.filter(f => f.RelatedEntityID != null && UUIDsEqual(f.RelatedEntityID, entity.ID));
1759
1949
  for (const field of recursiveFKs) {
1950
+ const metaSQL = this.generateHierarchyMetaFunction(entity, field);
1951
+ if (metaSQL && metaSQL.trim().length > 0) {
1952
+ await client.query(metaSQL);
1953
+ }
1954
+ const descSQL = this.generateDescendantsFunction(entity, field);
1955
+ if (descSQL && descSQL.trim().length > 0) {
1956
+ await client.query(descSQL);
1957
+ }
1958
+ const ancSQL = this.generateAncestorsFunction(entity, field);
1959
+ if (ancSQL && ancSQL.trim().length > 0) {
1960
+ await client.query(ancSQL);
1961
+ }
1760
1962
  const fnSQL = this.generateRootIDFunction(entity, field);
1761
1963
  if (fnSQL && fnSQL.trim().length > 0) {
1762
1964
  await client.query(fnSQL);
@@ -2232,102 +2434,6 @@ WHERE p.prokind IN ('f', 'p')
2232
2434
  AND tablename = '${tableName}'
2233
2435
  AND indexname = '${indexName}'`;
2234
2436
  }
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
2437
  // ─── COMPLEX SQL GENERATION HELPERS ──────────────────────────────
2332
2438
  /**
2333
2439
  * Builds the full pending entity fields query for PostgreSQL.
@@ -2461,7 +2567,7 @@ WHERE vf."IsVirtual" = true
2461
2567
  `;
2462
2568
  }
2463
2569
  };
2464
- PostgreSQLCodeGenProvider = PostgreSQLCodeGenProvider_1 = __decorate([
2570
+ PostgreSQLCodeGenProvider = __decorate([
2465
2571
  RegisterClass(CodeGenDatabaseProvider, 'postgresql')
2466
2572
  ], PostgreSQLCodeGenProvider);
2467
2573
  export { PostgreSQLCodeGenProvider };