@memberjunction/codegen-lib 6.1.0-edge.1 → 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 (96) 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 +107 -1
  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 +354 -14
  16. package/dist/Database/manage-metadata.d.ts.map +1 -1
  17. package/dist/Database/manage-metadata.js +1308 -65
  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 +69 -30
  44. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts.map +1 -1
  45. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js +325 -216
  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 +61 -2
  56. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts.map +1 -1
  57. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js +306 -3
  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 +85 -27
  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/createNewUser.js +1 -1
  83. package/dist/Misc/createNewUser.js.map +1 -1
  84. package/dist/Misc/entity_subclasses_codegen.d.ts +44 -0
  85. package/dist/Misc/entity_subclasses_codegen.d.ts.map +1 -1
  86. package/dist/Misc/entity_subclasses_codegen.js +369 -156
  87. package/dist/Misc/entity_subclasses_codegen.js.map +1 -1
  88. package/dist/Misc/graphql_server_codegen.d.ts +4 -58
  89. package/dist/Misc/graphql_server_codegen.d.ts.map +1 -1
  90. package/dist/Misc/graphql_server_codegen.js +22 -236
  91. package/dist/Misc/graphql_server_codegen.js.map +1 -1
  92. package/dist/Misc/sql_logging.d.ts +25 -0
  93. package/dist/Misc/sql_logging.d.ts.map +1 -1
  94. package/dist/Misc/sql_logging.js +51 -8
  95. package/dist/Misc/sql_logging.js.map +1 -1
  96. package/package.json +30 -30
@@ -4,15 +4,14 @@ 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
- import { EntityInfo, ResolveStartupMode, SetProvider, StartupManager, UserInfo } from '@memberjunction/core';
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';
15
- import { shouldIncludeFieldInParams, useJsonArgShape, } from '@memberjunction/generic-database-provider';
13
+ import { PostgreSQLDialect, AutoQuotePostgreSQLIdentifiers } from '@memberjunction/sql-dialect';
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';
18
17
  import { PostgreSQLCodeGenConnection } from './PostgreSQLCodeGenConnection.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;
@@ -47,14 +45,6 @@ let PostgreSQLCodeGenProvider = class PostgreSQLCodeGenProvider extends CodeGenD
47
45
  * wires up `PostgreSQLDataProvider`, registers it as the active provider,
48
46
  * and loads the audit user.
49
47
  *
50
- * **User-loading asymmetry** — SQL Server uses `UserCache.Instance.Refresh(pool)`
51
- * which is hard-typed to `mssql.ConnectionPool` in `@memberjunction/sqlserver-dataprovider`.
52
- * Refactoring it to be cross-platform would touch that package's public
53
- * API; until then PG hand-queries `vwUsers`/`vwUserRoles` here. Same
54
- * audit-user semantics (find Owner, else first user), just a different
55
- * load path. Tracked for follow-up: unify behind a platform-agnostic
56
- * cache that takes a `CodeGenConnection`.
57
- *
58
48
  * **Env var resolution** — PG_HOST / PG_PORT / PG_DATABASE / PG_USERNAME /
59
49
  * PG_PASSWORD now flow through `configInfo.{dbHost,dbPort,dbDatabase,codeGenLogin,codeGenPassword}`
60
50
  * via `DEFAULT_CODEGEN_CONFIG` (see `Config/config.ts`). The provider just
@@ -76,14 +66,9 @@ let PostgreSQLCodeGenProvider = class PostgreSQLCodeGenProvider extends CodeGenD
76
66
  await provider.Config(dpConfig);
77
67
  SetProvider(provider);
78
68
  const conn = new PostgreSQLCodeGenConnection(pool);
79
- const usersResult = await conn.query('SELECT * FROM "' + coreSchema + '"."vwUsers"');
80
- const rolesResult = await conn.query('SELECT * FROM "' + coreSchema + '"."vwUserRoles"');
81
- const userInfos = usersResult.recordset.map((user) => {
82
- user.UserRoles = rolesResult.recordset.filter((role) => UUIDsEqual(role.UserID, user.ID));
83
- return new UserInfo(provider, user);
84
- });
85
- const userMatch = userInfos.find((u) => u?.Type?.trim().toLowerCase() === 'owner');
86
- const currentUser = userMatch ?? userInfos[0];
69
+ await UserCache.Instance.Refresh(provider);
70
+ const userMatch = UserCache.Users.find((u) => u?.Type?.trim().toLowerCase() === 'owner');
71
+ const currentUser = userMatch ?? UserCache.Users[0];
87
72
  if (!currentUser) {
88
73
  throw new Error('No users found in PostgreSQL. Ensure vwUsers has at least one user.');
89
74
  }
@@ -395,6 +380,63 @@ EXCEPTION WHEN invalid_table_definition THEN
395
380
  END $vw_regen$;
396
381
  `;
397
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
+ }
398
440
  // ─── CRUD CREATE ─────────────────────────────────────────────────────
399
441
  /**
400
442
  * Emits a `DO` block that drops every existing overload of `<schema>.<fnName>`.
@@ -968,7 +1010,231 @@ WHERE ${ftsColName} IS NULL;
968
1010
  * the parent FK upward, capped at 100 levels to prevent infinite loops. Returns the
969
1011
  * root record's primary key value.
970
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
+ */
971
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
+ }
972
1238
  const primaryKey = entity.FirstPrimaryKey.Name;
973
1239
  const primaryKeyType = this.mapSQLType(entity.FirstPrimaryKey.SQLFullType);
974
1240
  const fieldName = field.Name;
@@ -1319,9 +1585,21 @@ END $$;
1319
1585
  }
1320
1586
  // ─── METADATA MANAGEMENT: STORED PROCEDURE CALLS ─────────────────
1321
1587
  /** @inheritdoc */
1322
- callRoutineSQL(schema, routineName, params, _paramNames) {
1588
+ callRoutineSQL(schema, routineName, params, _paramNames, discardResult) {
1323
1589
  const qualifiedName = pgDialect.QuoteSchema(schema, routineName);
1324
1590
  const paramList = params.join(', ');
1591
+ if (discardResult) {
1592
+ // `SELECT * FROM routine(...)` is not universally valid on PostgreSQL: a function
1593
+ // declared `RETURNS SETOF record` — which spDeleteEntityWithCoreDependencies is — is
1594
+ // rejected with "a column definition list is required for functions returning record",
1595
+ // and a work-performing routine has no column list to supply. PERFORM runs the function
1596
+ // and discards whatever it returns, which is exactly what these callers want.
1597
+ //
1598
+ // Left broken, this fails in a way that points somewhere else entirely: the
1599
+ // entity-pruning pass throws per entity, CodeGen logs "Error removing metadata for
1600
+ // entity undefined" and continues, and the run reports success having pruned nothing.
1601
+ return `DO $$ BEGIN PERFORM ${qualifiedName}(${paramList}); END $$`;
1602
+ }
1325
1603
  return `SELECT * FROM ${qualifiedName}(${paramList})`;
1326
1604
  }
1327
1605
  // ─── METADATA MANAGEMENT: CONDITIONAL INSERT ─────────────────────
@@ -1500,107 +1778,18 @@ $if_view_exists$;
1500
1778
  return true;
1501
1779
  }
1502
1780
  // ─── METADATA MANAGEMENT: SQL QUOTING ────────────────────────────
1503
- /**
1504
- * SQL keywords that should NOT be quoted even when they match PascalCase patterns.
1505
- */
1506
- static { this._SQL_KEYWORDS = new Set([
1507
- // DML/DDL keywords
1508
- 'SELECT', 'INSERT', 'INTO', 'UPDATE', 'DELETE', 'FROM', 'WHERE', 'AND', 'OR', 'NOT',
1509
- 'JOIN', 'LEFT', 'RIGHT', 'INNER', 'OUTER', 'CROSS', 'FULL', 'ON', 'AS', 'SET',
1510
- 'VALUES', 'NULL', 'LIKE', 'IN', 'EXISTS', 'BETWEEN', 'CASE', 'WHEN', 'THEN',
1511
- 'ELSE', 'END', 'ORDER', 'BY', 'GROUP', 'HAVING', 'LIMIT', 'OFFSET', 'UNION',
1512
- 'ALL', 'CREATE', 'ALTER', 'DROP', 'TABLE', 'INDEX', 'VIEW', 'EXEC', 'DECLARE',
1513
- 'BEGIN', 'COMMIT', 'ROLLBACK', 'TRANSACTION', 'TRUE', 'FALSE', 'IS', 'ASC', 'DESC',
1514
- 'DISTINCT', 'PRIMARY', 'KEY', 'FOREIGN', 'REFERENCES', 'CONSTRAINT', 'DEFAULT',
1515
- 'IF', 'OBJECT', 'TOP', 'WITH', 'OVER', 'PARTITION', 'ROW_NUMBER', 'RANK',
1516
- 'DENSE_RANK', 'LAG', 'LEAD', 'FIRST_VALUE', 'LAST_VALUE', 'ROWS', 'RANGE',
1517
- 'PRECEDING', 'FOLLOWING', 'UNBOUNDED', 'CURRENT', 'ROW', 'FETCH', 'NEXT', 'ONLY',
1518
- 'SCHEMA', 'CASCADE', 'RESTRICT', 'NO', 'ACTION', 'TRIGGER', 'FUNCTION', 'PROCEDURE',
1519
- 'RETURNS', 'RETURN', 'EXECUTE', 'CALL', 'RAISE', 'NOTICE', 'EXCEPTION', 'PERFORM',
1520
- 'GRANT', 'REVOKE', 'TO', 'USAGE', 'PRIVILEGES', 'OWNER',
1521
- // DDL sub-keywords
1522
- 'ADD', 'COLUMN', 'DO', 'RENAME', 'COMMENT', 'UNIQUE', 'CHECK',
1523
- 'CONFLICT', 'NOTHING', 'EXCLUDED', 'ZONE', 'AT', 'FOR', 'EACH', 'OF',
1524
- 'BEFORE', 'AFTER', 'INSTEAD', 'USING', 'ANY', 'SOME',
1525
- 'ENABLE', 'DISABLE', 'GENERATED', 'ALWAYS', 'IDENTITY',
1526
- 'SECURITY', 'DEFINER', 'INVOKER', 'FORCE', 'COPY',
1527
- 'TEMPORARY', 'TEMP', 'RECURSIVE', 'MATERIALIZED', 'CONCURRENTLY',
1528
- // PL/pgSQL control flow
1529
- 'NEW', 'OLD', 'FOUND', 'LOOP', 'WHILE', 'EXIT', 'CONTINUE',
1530
- 'ELSIF', 'ELSEIF', 'STRICT',
1531
- // Transaction / constraint control (used by SET CONSTRAINTS ALL IMMEDIATE
1532
- // emitted before ALTER TABLE so deferred trigger events flush). Without
1533
- // CONSTRAINTS / IMMEDIATE / DEFERRED in the keyword set, the tokenizer
1534
- // double-quotes them as identifiers and PG rejects the resulting SQL.
1535
- 'CONSTRAINTS', 'IMMEDIATE', 'DEFERRED', 'SAVEPOINT', 'RELEASE',
1536
- // SQL Server types
1537
- 'NVARCHAR', 'VARCHAR', 'UNIQUEIDENTIFIER', 'DATETIMEOFFSET', 'DATETIME', 'DATETIME2',
1538
- 'BIGINT', 'SMALLINT', 'TINYINT', 'FLOAT', 'REAL', 'DECIMAL', 'NUMERIC', 'MONEY',
1539
- 'BIT', 'INT', 'TEXT', 'NTEXT', 'IMAGE', 'BINARY', 'VARBINARY', 'CHAR', 'NCHAR',
1540
- 'XML', 'GEOGRAPHY', 'GEOMETRY', 'HIERARCHYID', 'SQL_VARIANT', 'SYSNAME',
1541
- 'NEWSEQUENTIALID', 'NEWID', 'GETUTCDATE', 'GETDATE', 'SYSDATETIMEOFFSET',
1542
- 'OBJECT_ID', 'SCOPE_IDENTITY',
1543
- // Aggregate / scalar functions
1544
- 'COUNT', 'MAX', 'MIN', 'SUM', 'AVG', 'COALESCE', 'CAST', 'CONVERT', 'ISNULL',
1545
- 'LEN', 'LENGTH', 'DATALENGTH', 'LOWER', 'UPPER', 'LTRIM', 'RTRIM', 'TRIM', 'REPLACE',
1546
- 'SUBSTRING', 'CHARINDEX', 'PATINDEX', 'STUFF', 'CONCAT', 'FORMAT',
1547
- 'LEFT', 'RIGHT', 'POSITION', 'OVERLAY', 'EXTRACT', 'GREATEST', 'LEAST',
1548
- 'DATEADD', 'DATEDIFF', 'DATEPART', 'YEAR', 'MONTH', 'DAY', 'HOUR', 'MINUTE',
1549
- 'SECOND', 'NOW', 'CURRENT_TIMESTAMP',
1550
- // PostgreSQL specific
1551
- 'BOOLEAN', 'SERIAL', 'BIGSERIAL', 'UUID', 'JSONB', 'JSON', 'ARRAY', 'TIMESTAMPTZ',
1552
- 'TIMESTAMP', 'DATE', 'TIME', 'INTERVAL', 'CITEXT', 'INET', 'MACADDR',
1553
- 'GEN_RANDOM_UUID', 'TO_CHAR', 'TO_DATE', 'TO_TIMESTAMP', 'TO_NUMBER',
1554
- 'STRING_AGG', 'ARRAY_AGG', 'UNNEST', 'LATERAL', 'ILIKE',
1555
- 'LANGUAGE', 'PLPGSQL', 'VOLATILE', 'STABLE', 'IMMUTABLE', 'SETOF', 'RECORD',
1556
- 'INOUT', 'OUT', 'VARIADIC', 'PARALLEL', 'SAFE', 'UNSAFE',
1557
- // information_schema column names
1558
- 'TABLE_SCHEMA', 'TABLE_NAME', 'TABLE_CATALOG', 'COLUMN_NAME', 'DATA_TYPE',
1559
- 'IS_NULLABLE', 'COLUMN_DEFAULT', 'CHARACTER_MAXIMUM_LENGTH', 'NUMERIC_PRECISION',
1560
- 'NUMERIC_SCALE', 'ORDINAL_POSITION', 'COLUMN_COMMENT',
1561
- // MJ SQL constructs
1562
- 'INFORMATION_SCHEMA', 'COLUMNS', 'TABLES', 'ROUTINES',
1563
- ]); }
1564
1781
  /**
1565
1782
  * Quotes mixed-case identifiers in a SQL string for PostgreSQL compatibility.
1566
- * Uses a tokenizer approach to skip string literals, already-quoted identifiers,
1567
- * dollar-quoted blocks, and SQL keywords. Any remaining PascalCase word gets
1568
- * 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
1569
1790
  */
1570
1791
  quoteSQLForExecution(sql) {
1571
- const result = [];
1572
- let i = 0;
1573
- const len = sql.length;
1574
- while (i < len) {
1575
- const ch = sql[i];
1576
- if (ch === "'") {
1577
- i = this.skipSingleQuotedString(sql, i, len, result);
1578
- continue;
1579
- }
1580
- if (ch === '$') {
1581
- i = this.skipDollarQuotedBlock(sql, i, len, result);
1582
- continue;
1583
- }
1584
- if (ch === '"') {
1585
- i = this.skipDoubleQuotedIdentifier(sql, i, len, result);
1586
- continue;
1587
- }
1588
- if (ch === '[') {
1589
- i = this.skipBracketedIdentifier(sql, i, len, result);
1590
- continue;
1591
- }
1592
- if (ch === '@') {
1593
- i = this.skipAtParameter(sql, i, len, result);
1594
- continue;
1595
- }
1596
- if (/[a-zA-Z_]/.test(ch)) {
1597
- i = this.processWord(sql, i, len, result);
1598
- continue;
1599
- }
1600
- result.push(ch);
1601
- i++;
1602
- }
1603
- return result.join('');
1792
+ return AutoQuotePostgreSQLIdentifiers(sql);
1604
1793
  }
1605
1794
  // ─── METADATA MANAGEMENT: DEFAULT VALUE PARSING ──────────────────
1606
1795
  /**
@@ -1758,6 +1947,18 @@ WHERE p.prokind IN ('f', 'p')
1758
1947
  // in the dialect-agnostic recovery loop.
1759
1948
  const recursiveFKs = entity.Fields.filter(f => f.RelatedEntityID != null && UUIDsEqual(f.RelatedEntityID, entity.ID));
1760
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
+ }
1761
1962
  const fnSQL = this.generateRootIDFunction(entity, field);
1762
1963
  if (fnSQL && fnSQL.trim().length > 0) {
1763
1964
  await client.query(fnSQL);
@@ -2233,102 +2434,6 @@ WHERE p.prokind IN ('f', 'p')
2233
2434
  AND tablename = '${tableName}'
2234
2435
  AND indexname = '${indexName}'`;
2235
2436
  }
2236
- // ─── TOKENIZER HELPERS (for quoteSQLForExecution) ────────────────
2237
- /** Skips a single-quoted string literal, handling escaped quotes ('') */
2238
- skipSingleQuotedString(sql, start, len, result) {
2239
- let j = start + 1;
2240
- while (j < len) {
2241
- if (sql[j] === "'" && j + 1 < len && sql[j + 1] === "'") {
2242
- j += 2;
2243
- }
2244
- else if (sql[j] === "'") {
2245
- j++;
2246
- break;
2247
- }
2248
- else {
2249
- j++;
2250
- }
2251
- }
2252
- result.push(sql.substring(start, j));
2253
- return j;
2254
- }
2255
- /** Skips a dollar-quoted block ($$ ... $$ or $tag$ ... $tag$) */
2256
- skipDollarQuotedBlock(sql, start, len, result) {
2257
- let tagEnd = start + 1;
2258
- if (tagEnd < len && sql[tagEnd] === '$') {
2259
- // Simple $$ tag
2260
- tagEnd = start + 2;
2261
- }
2262
- else {
2263
- // Look for $identifier$ pattern
2264
- while (tagEnd < len && /[a-zA-Z0-9_]/.test(sql[tagEnd]))
2265
- tagEnd++;
2266
- if (tagEnd < len && sql[tagEnd] === '$') {
2267
- tagEnd++;
2268
- }
2269
- else {
2270
- // Not a dollar-quote, just a $ character
2271
- result.push(sql[start]);
2272
- return start + 1;
2273
- }
2274
- }
2275
- const tag = sql.substring(start, tagEnd);
2276
- const closePos = sql.indexOf(tag, tagEnd);
2277
- if (closePos !== -1) {
2278
- const blockEnd = closePos + tag.length;
2279
- result.push(sql.substring(start, blockEnd));
2280
- return blockEnd;
2281
- }
2282
- // No closing tag found, pass through rest of string
2283
- result.push(sql.substring(start));
2284
- return len;
2285
- }
2286
- /** Skips an already double-quoted identifier */
2287
- skipDoubleQuotedIdentifier(sql, start, len, result) {
2288
- let j = start + 1;
2289
- while (j < len && sql[j] !== '"')
2290
- j++;
2291
- if (j < len)
2292
- j++;
2293
- result.push(sql.substring(start, j));
2294
- return j;
2295
- }
2296
- /** Skips a square-bracketed identifier (SQL Server style) */
2297
- skipBracketedIdentifier(sql, start, len, result) {
2298
- let j = start + 1;
2299
- while (j < len && sql[j] !== ']')
2300
- j++;
2301
- if (j < len)
2302
- j++;
2303
- result.push(sql.substring(start, j));
2304
- return j;
2305
- }
2306
- /** Skips an @-prefixed parameter */
2307
- skipAtParameter(sql, start, len, result) {
2308
- let j = start + 1;
2309
- while (j < len && /[a-zA-Z0-9_]/.test(sql[j]))
2310
- j++;
2311
- result.push(sql.substring(start, j));
2312
- return j;
2313
- }
2314
- /** Processes a word token - quotes it if it's a PascalCase identifier, not a keyword */
2315
- processWord(sql, start, len, result) {
2316
- let j = start + 1;
2317
- while (j < len && /[a-zA-Z0-9_]/.test(sql[j]))
2318
- j++;
2319
- const word = sql.substring(start, j);
2320
- const isKeyword = PostgreSQLCodeGenProvider_1._SQL_KEYWORDS.has(word.toUpperCase());
2321
- const startsUpper = /^[A-Z]/.test(word);
2322
- const isAllLower = word === word.toLowerCase();
2323
- const isMJInternal = word.startsWith('__mj_');
2324
- if (!isKeyword && !isAllLower && !isMJInternal && startsUpper) {
2325
- result.push(pgDialect.QuoteIdentifier(word));
2326
- }
2327
- else {
2328
- result.push(word);
2329
- }
2330
- return j;
2331
- }
2332
2437
  // ─── COMPLEX SQL GENERATION HELPERS ──────────────────────────────
2333
2438
  /**
2334
2439
  * Builds the full pending entity fields query for PostgreSQL.
@@ -2367,6 +2472,10 @@ numbered_rows AS (
2367
2472
  SELECT
2368
2473
  sf."EntityID",
2369
2474
  COALESCE(ms."MaxSequence", 0) + 100000 + sf."Sequence" AS "Sequence",
2475
+ -- The RAW schema ordinal, carried alongside the temporary Sequence above. The INSERT emitter
2476
+ -- adds it to an apply-time MAX(), so the ordering of newly discovered fields is encoded in the
2477
+ -- emitted VALUE rather than depending on the order the INSERT statements happen to execute.
2478
+ sf."Sequence" AS "SourceOrdinal",
2370
2479
  sf."FieldName",
2371
2480
  sf."Description",
2372
2481
  sf."Type",
@@ -2458,7 +2567,7 @@ WHERE vf."IsVirtual" = true
2458
2567
  `;
2459
2568
  }
2460
2569
  };
2461
- PostgreSQLCodeGenProvider = PostgreSQLCodeGenProvider_1 = __decorate([
2570
+ PostgreSQLCodeGenProvider = __decorate([
2462
2571
  RegisterClass(CodeGenDatabaseProvider, 'postgresql')
2463
2572
  ], PostgreSQLCodeGenProvider);
2464
2573
  export { PostgreSQLCodeGenProvider };