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