@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.
- package/LICENSE +180 -4
- package/README.md +39 -22
- package/dist/Angular/angular-codegen.d.ts +0 -8
- package/dist/Angular/angular-codegen.d.ts.map +1 -1
- package/dist/Angular/angular-codegen.js +32 -22
- package/dist/Angular/angular-codegen.js.map +1 -1
- package/dist/Angular/entity-data-grid-related-entity-component.js +2 -2
- package/dist/Angular/entity-data-grid-related-entity-component.js.map +1 -1
- package/dist/Angular/related-entity-components.d.ts +1 -1
- package/dist/Angular/related-entity-components.js +1 -1
- package/dist/Database/codeGenDatabaseProvider.d.ts +107 -1
- package/dist/Database/codeGenDatabaseProvider.d.ts.map +1 -1
- package/dist/Database/codeGenDatabaseProvider.js +77 -10
- package/dist/Database/codeGenDatabaseProvider.js.map +1 -1
- package/dist/Database/manage-metadata.d.ts +354 -14
- package/dist/Database/manage-metadata.d.ts.map +1 -1
- package/dist/Database/manage-metadata.js +1308 -65
- package/dist/Database/manage-metadata.js.map +1 -1
- package/dist/Database/materializationAnalysis.d.ts +241 -0
- package/dist/Database/materializationAnalysis.d.ts.map +1 -0
- package/dist/Database/materializationAnalysis.js +415 -0
- package/dist/Database/materializationAnalysis.js.map +1 -0
- package/dist/Database/materializationBroadRender.d.ts +67 -0
- package/dist/Database/materializationBroadRender.d.ts.map +1 -0
- package/dist/Database/materializationBroadRender.js +104 -0
- package/dist/Database/materializationBroadRender.js.map +1 -0
- package/dist/Database/materializationDrift.d.ts +59 -0
- package/dist/Database/materializationDrift.d.ts.map +1 -0
- package/dist/Database/materializationDrift.js +85 -0
- package/dist/Database/materializationDrift.js.map +1 -0
- package/dist/Database/materializationParamClassifier.d.ts +92 -0
- package/dist/Database/materializationParamClassifier.d.ts.map +1 -0
- package/dist/Database/materializationParamClassifier.js +224 -0
- package/dist/Database/materializationParamClassifier.js.map +1 -0
- package/dist/Database/materializationParamVerifier.d.ts +71 -0
- package/dist/Database/materializationParamVerifier.d.ts.map +1 -0
- package/dist/Database/materializationParamVerifier.js +302 -0
- package/dist/Database/materializationParamVerifier.js.map +1 -0
- package/dist/Database/materializationSqlAst.d.ts +78 -0
- package/dist/Database/materializationSqlAst.d.ts.map +1 -0
- package/dist/Database/materializationSqlAst.js +142 -0
- package/dist/Database/materializationSqlAst.js.map +1 -0
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts +69 -30
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts.map +1 -1
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js +325 -216
- package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js.map +1 -1
- package/dist/Database/providers/postgresql/__tests__/PostgreSQLCodeGenProvider.materialization.test.d.ts +2 -0
- package/dist/Database/providers/postgresql/__tests__/PostgreSQLCodeGenProvider.materialization.test.d.ts.map +1 -0
- package/dist/Database/providers/postgresql/__tests__/PostgreSQLCodeGenProvider.materialization.test.js +119 -0
- package/dist/Database/providers/postgresql/__tests__/PostgreSQLCodeGenProvider.materialization.test.js.map +1 -0
- package/dist/Database/providers/postgresql/__tests__/PostgreSQLHierarchyFunctions.test.d.ts +2 -0
- package/dist/Database/providers/postgresql/__tests__/PostgreSQLHierarchyFunctions.test.d.ts.map +1 -0
- package/dist/Database/providers/postgresql/__tests__/PostgreSQLHierarchyFunctions.test.js +155 -0
- package/dist/Database/providers/postgresql/__tests__/PostgreSQLHierarchyFunctions.test.js.map +1 -0
- package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts +61 -2
- package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts.map +1 -1
- package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js +306 -3
- package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js.map +1 -1
- package/dist/Database/providers/sqlserver/__tests__/BaseViewEmissionGoldenMaster.test.d.ts +2 -0
- package/dist/Database/providers/sqlserver/__tests__/BaseViewEmissionGoldenMaster.test.d.ts.map +1 -0
- package/dist/Database/providers/sqlserver/__tests__/BaseViewEmissionGoldenMaster.test.js +547 -0
- package/dist/Database/providers/sqlserver/__tests__/BaseViewEmissionGoldenMaster.test.js.map +1 -0
- package/dist/Database/providers/sqlserver/__tests__/BaseViewRegenDecision.test.d.ts +2 -0
- package/dist/Database/providers/sqlserver/__tests__/BaseViewRegenDecision.test.d.ts.map +1 -0
- package/dist/Database/providers/sqlserver/__tests__/BaseViewRegenDecision.test.js +721 -0
- package/dist/Database/providers/sqlserver/__tests__/BaseViewRegenDecision.test.js.map +1 -0
- package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.materialization.test.d.ts +2 -0
- package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.materialization.test.d.ts.map +1 -0
- package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.materialization.test.js +78 -0
- package/dist/Database/providers/sqlserver/__tests__/SQLServerCodeGenProvider.materialization.test.js.map +1 -0
- package/dist/Database/providers/sqlserver/__tests__/SQLServerHierarchyTVF.test.d.ts +2 -0
- package/dist/Database/providers/sqlserver/__tests__/SQLServerHierarchyTVF.test.d.ts.map +1 -0
- package/dist/Database/providers/sqlserver/__tests__/SQLServerHierarchyTVF.test.js +155 -0
- package/dist/Database/providers/sqlserver/__tests__/SQLServerHierarchyTVF.test.js.map +1 -0
- package/dist/Database/sql_codegen.d.ts +21 -5
- package/dist/Database/sql_codegen.d.ts.map +1 -1
- package/dist/Database/sql_codegen.js +85 -27
- package/dist/Database/sql_codegen.js.map +1 -1
- package/dist/Misc/action_subclasses_codegen.d.ts.map +1 -1
- package/dist/Misc/action_subclasses_codegen.js +4 -4
- package/dist/Misc/action_subclasses_codegen.js.map +1 -1
- package/dist/Misc/createNewUser.js +1 -1
- package/dist/Misc/createNewUser.js.map +1 -1
- package/dist/Misc/entity_subclasses_codegen.d.ts +44 -0
- package/dist/Misc/entity_subclasses_codegen.d.ts.map +1 -1
- package/dist/Misc/entity_subclasses_codegen.js +369 -156
- package/dist/Misc/entity_subclasses_codegen.js.map +1 -1
- package/dist/Misc/graphql_server_codegen.d.ts +4 -58
- package/dist/Misc/graphql_server_codegen.d.ts.map +1 -1
- package/dist/Misc/graphql_server_codegen.js +22 -236
- package/dist/Misc/graphql_server_codegen.js.map +1 -1
- package/dist/Misc/sql_logging.d.ts +25 -0
- package/dist/Misc/sql_logging.d.ts.map +1 -1
- package/dist/Misc/sql_logging.js +51 -8
- package/dist/Misc/sql_logging.js.map +1 -1
- 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
|
-
|
|
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
|
-
|
|
80
|
-
const
|
|
81
|
-
const
|
|
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
|
-
*
|
|
1567
|
-
*
|
|
1568
|
-
*
|
|
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
|
-
|
|
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 =
|
|
2570
|
+
PostgreSQLCodeGenProvider = __decorate([
|
|
2462
2571
|
RegisterClass(CodeGenDatabaseProvider, 'postgresql')
|
|
2463
2572
|
], PostgreSQLCodeGenProvider);
|
|
2464
2573
|
export { PostgreSQLCodeGenProvider };
|