@memberjunction/integration-schema-builder 6.2.0-edge.0 → 6.2.0-edge.1
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/dist/DDLGenerator.d.ts +4 -4
- package/dist/DDLGenerator.js +10 -10
- package/dist/SchemaBuilder.d.ts +14 -14
- package/dist/SchemaBuilder.js +37 -37
- package/dist/SchemaEvolution.d.ts +3 -3
- package/dist/SchemaEvolution.js +6 -6
- package/package.json +6 -6
package/dist/DDLGenerator.d.ts
CHANGED
|
@@ -22,12 +22,12 @@ export declare class DDLGenerator {
|
|
|
22
22
|
* "not sqlserver" catch-all — and the `never` default makes adding a new DatabasePlatform
|
|
23
23
|
* a compile error here rather than a silent fall-through to the wrong dialect.
|
|
24
24
|
*/
|
|
25
|
-
private
|
|
25
|
+
private generateIdempotentCreateTableStatement;
|
|
26
26
|
/**
|
|
27
27
|
* Generate sp_addextendedproperty calls for table and column descriptions.
|
|
28
28
|
*/
|
|
29
29
|
GenerateExtendedProperties(config: TargetTableConfig): string[];
|
|
30
|
-
private
|
|
30
|
+
private makeColumnExtendedProperty;
|
|
31
31
|
/**
|
|
32
32
|
* Wrap an sp_addextendedproperty EXEC in an existence guard so re-running a migration is
|
|
33
33
|
* idempotent — without it, re-adding MS_Description to a table/column that already has it
|
|
@@ -36,7 +36,7 @@ export declare class DDLGenerator {
|
|
|
36
36
|
* postgresql uses no extended properties here. Pass columnName=null for a table-level
|
|
37
37
|
* property, or the column name for a column-level one (levels must match the EXEC).
|
|
38
38
|
*/
|
|
39
|
-
private
|
|
39
|
+
private guardExtendedProperty;
|
|
40
40
|
/**
|
|
41
41
|
* Generate ALTER TABLE ADD COLUMN statement.
|
|
42
42
|
*/
|
|
@@ -45,7 +45,7 @@ export declare class DDLGenerator {
|
|
|
45
45
|
* Generate ALTER TABLE ALTER COLUMN statement for type/nullability changes.
|
|
46
46
|
*/
|
|
47
47
|
GenerateAlterTableAlterColumn(schemaName: string, tableName: string, mod: ColumnModification, platform: DatabasePlatform): string;
|
|
48
|
-
private
|
|
48
|
+
private standardColumns;
|
|
49
49
|
}
|
|
50
50
|
/** Validates that an identifier contains only safe characters. */
|
|
51
51
|
export declare function ValidateIdentifier(name: string, kind: string): void;
|
package/dist/DDLGenerator.js
CHANGED
|
@@ -47,7 +47,7 @@ export class DDLGenerator {
|
|
|
47
47
|
lines.push(` ${q(col.TargetColumnName)} ${sqlType} ${nullable}${defaultExpr}`);
|
|
48
48
|
}
|
|
49
49
|
// Standard integration columns (prefixed to avoid collisions)
|
|
50
|
-
lines.push(...this.
|
|
50
|
+
lines.push(...this.standardColumns(platform));
|
|
51
51
|
// PK is SOFT-ONLY — declared in additionalSchemaInfo (which CodeGen reads to set the entity's
|
|
52
52
|
// metadata PK), and emitted as NO database constraint here: not a PRIMARY KEY, and not even a
|
|
53
53
|
// UNIQUE. Integration PKs are *inferred* (naming + streamed-data statistics at p<0.05), so
|
|
@@ -59,7 +59,7 @@ export class DDLGenerator {
|
|
|
59
59
|
// Create-Tables safe even when MJ has no entity for the table yet (e.g. a prior run
|
|
60
60
|
// created the table but entity generation hadn't completed), avoiding
|
|
61
61
|
// "There is already an object named '<table>'" collisions.
|
|
62
|
-
const createTable = this.
|
|
62
|
+
const createTable = this.generateIdempotentCreateTableStatement(fullTable, body, platform);
|
|
63
63
|
// Generate extended properties for descriptions (SQL Server only)
|
|
64
64
|
if (platform === 'sqlserver') {
|
|
65
65
|
const extProps = this.GenerateExtendedProperties(config);
|
|
@@ -75,7 +75,7 @@ export class DDLGenerator {
|
|
|
75
75
|
* "not sqlserver" catch-all — and the `never` default makes adding a new DatabasePlatform
|
|
76
76
|
* a compile error here rather than a silent fall-through to the wrong dialect.
|
|
77
77
|
*/
|
|
78
|
-
|
|
78
|
+
generateIdempotentCreateTableStatement(fullTable, body, platform) {
|
|
79
79
|
switch (platform) {
|
|
80
80
|
case 'sqlserver':
|
|
81
81
|
// Single-statement IF guard (no BEGIN/END) so RSU's batch chunking on ';\n' can't split it.
|
|
@@ -102,7 +102,7 @@ export class DDLGenerator {
|
|
|
102
102
|
` @value = N'${escaped}',\n` +
|
|
103
103
|
` @level0type = N'SCHEMA', @level0name = '${config.SchemaName}',\n` +
|
|
104
104
|
` @level1type = N'TABLE', @level1name = '${config.TableName}';`;
|
|
105
|
-
props.push(this.
|
|
105
|
+
props.push(this.guardExtendedProperty(config.SchemaName, config.TableName, null, exec));
|
|
106
106
|
}
|
|
107
107
|
// Standard column descriptions
|
|
108
108
|
const standardDescriptions = {
|
|
@@ -114,17 +114,17 @@ export class DDLGenerator {
|
|
|
114
114
|
'__mj_integration_CustomOverflow': 'Backend staging (system) column: JSON of source fields a record returned that have no field map yet — the "extra" keys this table has no column for. A post-sync Runtime-Schema-Updation pass promotes pervasive keys to real columns and clears them here. Not user-facing metadata; transient until promotion.',
|
|
115
115
|
};
|
|
116
116
|
for (const [colName, desc] of Object.entries(standardDescriptions)) {
|
|
117
|
-
props.push(this.
|
|
117
|
+
props.push(this.makeColumnExtendedProperty(config.SchemaName, config.TableName, colName, desc));
|
|
118
118
|
}
|
|
119
119
|
// User-configured column descriptions
|
|
120
120
|
for (const col of config.Columns) {
|
|
121
121
|
if (col.Description) {
|
|
122
|
-
props.push(this.
|
|
122
|
+
props.push(this.makeColumnExtendedProperty(config.SchemaName, config.TableName, col.TargetColumnName, col.Description));
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
125
|
return props;
|
|
126
126
|
}
|
|
127
|
-
|
|
127
|
+
makeColumnExtendedProperty(schemaName, tableName, columnName, description) {
|
|
128
128
|
const escaped = EscapeSQLString(description);
|
|
129
129
|
const exec = `EXEC sp_addextendedproperty\n` +
|
|
130
130
|
` @name = N'MS_Description',\n` +
|
|
@@ -132,7 +132,7 @@ export class DDLGenerator {
|
|
|
132
132
|
` @level0type = N'SCHEMA', @level0name = '${schemaName}',\n` +
|
|
133
133
|
` @level1type = N'TABLE', @level1name = '${tableName}',\n` +
|
|
134
134
|
` @level2type = N'COLUMN', @level2name = '${columnName}';`;
|
|
135
|
-
return this.
|
|
135
|
+
return this.guardExtendedProperty(schemaName, tableName, columnName, exec);
|
|
136
136
|
}
|
|
137
137
|
/**
|
|
138
138
|
* Wrap an sp_addextendedproperty EXEC in an existence guard so re-running a migration is
|
|
@@ -142,7 +142,7 @@ export class DDLGenerator {
|
|
|
142
142
|
* postgresql uses no extended properties here. Pass columnName=null for a table-level
|
|
143
143
|
* property, or the column name for a column-level one (levels must match the EXEC).
|
|
144
144
|
*/
|
|
145
|
-
|
|
145
|
+
guardExtendedProperty(schemaName, tableName, columnName, execStatement) {
|
|
146
146
|
const level2 = columnName ? `N'COLUMN', N'${columnName}'` : `NULL, NULL`;
|
|
147
147
|
return (`IF NOT EXISTS (SELECT 1 FROM sys.fn_listextendedproperty(` +
|
|
148
148
|
`N'MS_Description', N'SCHEMA', N'${schemaName}', N'TABLE', N'${tableName}', ${level2}))\n` +
|
|
@@ -214,7 +214,7 @@ export class DDLGenerator {
|
|
|
214
214
|
`$mj_dropviews$;`;
|
|
215
215
|
return `${dropDependentViews}\n${alter}`;
|
|
216
216
|
}
|
|
217
|
-
|
|
217
|
+
standardColumns(platform) {
|
|
218
218
|
const q = platform === 'sqlserver' ? QuoteSqlServer : QuotePostgres;
|
|
219
219
|
if (platform === 'sqlserver') {
|
|
220
220
|
return [
|
package/dist/SchemaBuilder.d.ts
CHANGED
|
@@ -12,10 +12,10 @@ import { type RSUPipelineInput, type RSUPipelineResult } from '@memberjunction/s
|
|
|
12
12
|
* integration-specific artifacts (sync columns, soft FKs, metadata files).
|
|
13
13
|
*/
|
|
14
14
|
export declare class SchemaBuilder {
|
|
15
|
-
private readonly
|
|
16
|
-
private readonly
|
|
17
|
-
private readonly
|
|
18
|
-
private readonly
|
|
15
|
+
private readonly engine;
|
|
16
|
+
private readonly softFKEmitter;
|
|
17
|
+
private readonly metaEmitter;
|
|
18
|
+
private readonly evolution;
|
|
19
19
|
/**
|
|
20
20
|
* Build all artifacts for a set of integration target tables.
|
|
21
21
|
*/
|
|
@@ -53,9 +53,9 @@ export declare class SchemaBuilder {
|
|
|
53
53
|
* Convert an integration TargetTableConfig to a generic TableDefinition.
|
|
54
54
|
* Adds integration-specific sync columns via AdditionalColumns.
|
|
55
55
|
*/
|
|
56
|
-
private
|
|
56
|
+
private convertToTableDefinition;
|
|
57
57
|
/** Convert a TargetColumnConfig to a generic ColumnDefinition. */
|
|
58
|
-
private
|
|
58
|
+
private convertColumn;
|
|
59
59
|
/**
|
|
60
60
|
* Dialect-aware row-size guard. A dialect that reports a hard in-row size limit
|
|
61
61
|
* ({@link SQLDialect.MaxInRowSizeBytes} — e.g. SQL Server's ~8060 bytes) pushes only
|
|
@@ -71,19 +71,19 @@ export declare class SchemaBuilder {
|
|
|
71
71
|
* so there is no platform branching here. Mutates config.Columns in place to the kept subset so all
|
|
72
72
|
* downstream steps (DDL, soft FK, metadata) operate on the columns that actually exist.
|
|
73
73
|
*/
|
|
74
|
-
private
|
|
74
|
+
private capColumnsForRowSize;
|
|
75
75
|
/** Integration-specific sync columns added to every integration table. */
|
|
76
|
-
private
|
|
76
|
+
private integrationSyncColumns;
|
|
77
77
|
/**
|
|
78
78
|
* Extract DDL body from a wrapped migration file (strips the header comments).
|
|
79
79
|
* SchemaEngine wraps DDL in a migration header — we need just the DDL
|
|
80
80
|
* since we produce our own single consolidated migration file.
|
|
81
81
|
*/
|
|
82
|
-
private
|
|
83
|
-
private
|
|
84
|
-
private
|
|
85
|
-
private
|
|
86
|
-
private
|
|
87
|
-
private
|
|
82
|
+
private readFileIfExists;
|
|
83
|
+
private validateAccessControl;
|
|
84
|
+
private checkCollisions;
|
|
85
|
+
private classifyConfigs;
|
|
86
|
+
private getUniqueSchemas;
|
|
87
|
+
private collectSoftFKs;
|
|
88
88
|
}
|
|
89
89
|
//# sourceMappingURL=SchemaBuilder.d.ts.map
|
package/dist/SchemaBuilder.js
CHANGED
|
@@ -12,10 +12,10 @@ import { GetDialect } from '@memberjunction/sql-dialect';
|
|
|
12
12
|
*/
|
|
13
13
|
export class SchemaBuilder {
|
|
14
14
|
constructor() {
|
|
15
|
-
this.
|
|
16
|
-
this.
|
|
17
|
-
this.
|
|
18
|
-
this.
|
|
15
|
+
this.engine = new SchemaEngine();
|
|
16
|
+
this.softFKEmitter = new SoftFKConfigEmitter();
|
|
17
|
+
this.metaEmitter = new MetadataEmitter();
|
|
18
|
+
this.evolution = new SchemaEvolution();
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
21
|
* Build all artifacts for a set of integration target tables.
|
|
@@ -29,22 +29,22 @@ export class SchemaBuilder {
|
|
|
29
29
|
Errors: [],
|
|
30
30
|
};
|
|
31
31
|
// Step 1: Validate access control
|
|
32
|
-
const accessErrors = this.
|
|
32
|
+
const accessErrors = this.validateAccessControl(input);
|
|
33
33
|
if (accessErrors.length > 0) {
|
|
34
34
|
output.Errors.push(...accessErrors);
|
|
35
35
|
return output;
|
|
36
36
|
}
|
|
37
37
|
// Step 2: Pre-flight collision checks
|
|
38
|
-
const collisionWarnings = this.
|
|
38
|
+
const collisionWarnings = this.checkCollisions(input);
|
|
39
39
|
output.Warnings.push(...collisionWarnings);
|
|
40
40
|
// Step 3: Separate new tables vs evolution
|
|
41
|
-
const { newConfigs, evolutionConfigs } = this.
|
|
41
|
+
const { newConfigs, evolutionConfigs } = this.classifyConfigs(input);
|
|
42
42
|
// Step 4: Generate DDL using SchemaEngine
|
|
43
43
|
const ddlParts = [];
|
|
44
44
|
// Schema creation statements (deduplicate by schema name)
|
|
45
|
-
const schemasToCreate = this.
|
|
45
|
+
const schemasToCreate = this.getUniqueSchemas(newConfigs);
|
|
46
46
|
for (const schemaName of schemasToCreate) {
|
|
47
|
-
ddlParts.push(this.
|
|
47
|
+
ddlParts.push(this.engine.GenerateCreateSchema(schemaName, input.Platform));
|
|
48
48
|
ddlParts.push(''); // blank line separator
|
|
49
49
|
}
|
|
50
50
|
// Convert new tables → TableDefinition and generate CREATE TABLE via SchemaEngine DDLGenerator
|
|
@@ -57,8 +57,8 @@ export class SchemaBuilder {
|
|
|
57
57
|
// ~8060 bytes) makes very wide tables (600+ columns) impossible to INSERT into — cap to a
|
|
58
58
|
// fitting core subset + warn, rather than emit a doomed table. No-op on dialects with no
|
|
59
59
|
// in-row limit (PostgreSQL/TOAST). Mutates config.Columns.
|
|
60
|
-
output.Warnings.push(...this.
|
|
61
|
-
const tableDef = this.
|
|
60
|
+
output.Warnings.push(...this.capColumnsForRowSize(config, input.Platform));
|
|
61
|
+
const tableDef = this.convertToTableDefinition(config, input.Platform);
|
|
62
62
|
// IfNotExists: integration Create-Tables must be idempotent — a physical table
|
|
63
63
|
// may already exist with no MJ entity yet (e.g. a prior run created the table but
|
|
64
64
|
// CodeGen hadn't created the entity), and re-running must not collide.
|
|
@@ -70,14 +70,14 @@ export class SchemaBuilder {
|
|
|
70
70
|
const sourceObj = input.SourceSchema.Objects.find(o => o.ExternalName === config.SourceObjectName);
|
|
71
71
|
if (!sourceObj)
|
|
72
72
|
continue;
|
|
73
|
-
const diff = this.
|
|
73
|
+
const diff = this.evolution.DiffSchema(sourceObj, config, existing, input.Platform);
|
|
74
74
|
if (diff.Warnings.length > 0) {
|
|
75
75
|
output.Warnings.push(...diff.Warnings);
|
|
76
76
|
}
|
|
77
77
|
if (diff.AddedColumns.length === 0 && diff.ModifiedColumns.length === 0 && diff.RemovedColumns.length === 0) {
|
|
78
78
|
continue; // No changes
|
|
79
79
|
}
|
|
80
|
-
ddlParts.push(this.
|
|
80
|
+
ddlParts.push(this.evolution.GenerateEvolutionMigration(diff, config.SchemaName, config.TableName, input.Platform));
|
|
81
81
|
ddlParts.push(''); // blank line separator
|
|
82
82
|
}
|
|
83
83
|
// Emit a single migration file if there's any DDL
|
|
@@ -101,28 +101,28 @@ export class SchemaBuilder {
|
|
|
101
101
|
}
|
|
102
102
|
// Step 5: Generate soft PK and FK config
|
|
103
103
|
const allConfigs = [...newConfigs, ...evolutionConfigs.map(ec => ec.config)];
|
|
104
|
-
const allSoftFKs = this.
|
|
104
|
+
const allSoftFKs = this.collectSoftFKs(input, evolutionConfigs);
|
|
105
105
|
// Always emit additionalSchemaInfo — every integration table needs a soft PK
|
|
106
106
|
if (allConfigs.length > 0) {
|
|
107
|
-
const existingContent = this.
|
|
108
|
-
const existingConfig = this.
|
|
109
|
-
const withPKs = this.
|
|
107
|
+
const existingContent = this.readFileIfExists(input.AdditionalSchemaInfoPath);
|
|
108
|
+
const existingConfig = this.softFKEmitter.ParseExistingConfig(existingContent);
|
|
109
|
+
const withPKs = this.softFKEmitter.MergeSoftPKs(existingConfig, allConfigs);
|
|
110
110
|
// This run's PK/FK resolution REPLACES the prior run's entries for the
|
|
111
111
|
// tables it covers (adds new, removes gone) — clear their FKs before the rebuild so
|
|
112
112
|
// a stale FK never outlives the resolution that once declared it. Clearing happens
|
|
113
113
|
// even when allSoftFKs is empty (a table that lost every FK ends clear).
|
|
114
|
-
const cleared = this.
|
|
114
|
+
const cleared = this.softFKEmitter.ClearForeignKeysForTables(withPKs, allConfigs);
|
|
115
115
|
const merged = allSoftFKs.length > 0
|
|
116
|
-
? this.
|
|
116
|
+
? this.softFKEmitter.MergeSchemaConfig(cleared, allSoftFKs)
|
|
117
117
|
: cleared;
|
|
118
|
-
output.AdditionalSchemaInfoUpdate = this.
|
|
118
|
+
output.AdditionalSchemaInfoUpdate = this.softFKEmitter.EmitConfigFile(input.AdditionalSchemaInfoPath, merged);
|
|
119
119
|
}
|
|
120
120
|
// Step 6: Generate metadata files for __mj entity targets
|
|
121
121
|
const mjTargets = input.TargetConfigs.filter(c => c.SchemaName === '__mj');
|
|
122
122
|
if (mjTargets.length > 0) {
|
|
123
123
|
const entityNames = mjTargets.map(c => c.EntityName);
|
|
124
|
-
output.MetadataFiles.push(this.
|
|
125
|
-
output.MetadataFiles.push(this.
|
|
124
|
+
output.MetadataFiles.push(this.metaEmitter.EmitEntitySettingsFile(entityNames, input.MetadataDir));
|
|
125
|
+
output.MetadataFiles.push(this.metaEmitter.EmitMjSyncConfig(input.MetadataDir));
|
|
126
126
|
}
|
|
127
127
|
return output;
|
|
128
128
|
}
|
|
@@ -216,19 +216,19 @@ export class SchemaBuilder {
|
|
|
216
216
|
* Convert an integration TargetTableConfig to a generic TableDefinition.
|
|
217
217
|
* Adds integration-specific sync columns via AdditionalColumns.
|
|
218
218
|
*/
|
|
219
|
-
|
|
219
|
+
convertToTableDefinition(config, platform) {
|
|
220
220
|
return {
|
|
221
221
|
SchemaName: config.SchemaName,
|
|
222
222
|
TableName: config.TableName,
|
|
223
223
|
EntityName: config.EntityName,
|
|
224
224
|
Description: config.Description,
|
|
225
|
-
Columns: config.Columns.map(c => this.
|
|
225
|
+
Columns: config.Columns.map(c => this.convertColumn(c)),
|
|
226
226
|
SoftPrimaryKeys: config.PrimaryKeyFields,
|
|
227
|
-
AdditionalColumns: this.
|
|
227
|
+
AdditionalColumns: this.integrationSyncColumns(platform),
|
|
228
228
|
};
|
|
229
229
|
}
|
|
230
230
|
/** Convert a TargetColumnConfig to a generic ColumnDefinition. */
|
|
231
|
-
|
|
231
|
+
convertColumn(col) {
|
|
232
232
|
return {
|
|
233
233
|
Name: col.TargetColumnName,
|
|
234
234
|
Type: 'string', // Overridden by RawSqlType
|
|
@@ -256,7 +256,7 @@ export class SchemaBuilder {
|
|
|
256
256
|
* so there is no platform branching here. Mutates config.Columns in place to the kept subset so all
|
|
257
257
|
* downstream steps (DDL, soft FK, metadata) operate on the columns that actually exist.
|
|
258
258
|
*/
|
|
259
|
-
|
|
259
|
+
capColumnsForRowSize(config, platform) {
|
|
260
260
|
const dialect = GetDialect(platform);
|
|
261
261
|
const maxRowBytes = dialect.MaxInRowSizeBytes;
|
|
262
262
|
const maxCols = dialect.MaxColumnCount;
|
|
@@ -309,7 +309,7 @@ export class SchemaBuilder {
|
|
|
309
309
|
];
|
|
310
310
|
}
|
|
311
311
|
/** Integration-specific sync columns added to every integration table. */
|
|
312
|
-
|
|
312
|
+
integrationSyncColumns(platform) {
|
|
313
313
|
return [
|
|
314
314
|
{
|
|
315
315
|
Name: '__mj_integration_SyncStatus',
|
|
@@ -405,7 +405,7 @@ export class SchemaBuilder {
|
|
|
405
405
|
* SchemaEngine wraps DDL in a migration header — we need just the DDL
|
|
406
406
|
* since we produce our own single consolidated migration file.
|
|
407
407
|
*/
|
|
408
|
-
|
|
408
|
+
readFileIfExists(filePath) {
|
|
409
409
|
try {
|
|
410
410
|
if (existsSync(filePath)) {
|
|
411
411
|
return readFileSync(filePath, 'utf-8');
|
|
@@ -415,7 +415,7 @@ export class SchemaBuilder {
|
|
|
415
415
|
return null;
|
|
416
416
|
}
|
|
417
417
|
// ─── Validation & Classification ────────────────────────────────────
|
|
418
|
-
|
|
418
|
+
validateAccessControl(input) {
|
|
419
419
|
const errors = [];
|
|
420
420
|
for (const config of input.TargetConfigs) {
|
|
421
421
|
if (config.SchemaName === '__mj') {
|
|
@@ -428,7 +428,7 @@ export class SchemaBuilder {
|
|
|
428
428
|
}
|
|
429
429
|
return errors;
|
|
430
430
|
}
|
|
431
|
-
|
|
431
|
+
checkCollisions(input) {
|
|
432
432
|
const warnings = [];
|
|
433
433
|
const existingNames = new Set(input.ExistingTables.map(t => `${t.SchemaName}.${t.TableName}`.toLowerCase()));
|
|
434
434
|
for (const config of input.TargetConfigs) {
|
|
@@ -439,7 +439,7 @@ export class SchemaBuilder {
|
|
|
439
439
|
}
|
|
440
440
|
return warnings;
|
|
441
441
|
}
|
|
442
|
-
|
|
442
|
+
classifyConfigs(input) {
|
|
443
443
|
const existingMap = new Map();
|
|
444
444
|
for (const t of input.ExistingTables) {
|
|
445
445
|
existingMap.set(`${t.SchemaName}.${t.TableName}`.toLowerCase(), t);
|
|
@@ -458,23 +458,23 @@ export class SchemaBuilder {
|
|
|
458
458
|
}
|
|
459
459
|
return { newConfigs, evolutionConfigs };
|
|
460
460
|
}
|
|
461
|
-
|
|
461
|
+
getUniqueSchemas(configs) {
|
|
462
462
|
const schemas = new Set();
|
|
463
463
|
for (const config of configs) {
|
|
464
464
|
schemas.add(config.SchemaName);
|
|
465
465
|
}
|
|
466
466
|
return Array.from(schemas);
|
|
467
467
|
}
|
|
468
|
-
|
|
468
|
+
collectSoftFKs(input, evolutionConfigs) {
|
|
469
469
|
// From source relationships
|
|
470
|
-
const fromSource = this.
|
|
470
|
+
const fromSource = this.softFKEmitter.GenerateConfigEntries(input.SourceSchema, input.TargetConfigs);
|
|
471
471
|
// From evolution (new FK columns)
|
|
472
472
|
for (const { config, existing } of evolutionConfigs) {
|
|
473
473
|
const sourceObj = input.SourceSchema.Objects.find(o => o.ExternalName === config.SourceObjectName);
|
|
474
474
|
if (!sourceObj)
|
|
475
475
|
continue;
|
|
476
|
-
const diff = this.
|
|
477
|
-
const newFKs = this.
|
|
476
|
+
const diff = this.evolution.DiffSchema(sourceObj, config, existing, input.Platform);
|
|
477
|
+
const newFKs = this.evolution.GenerateEvolutionSoftFKUpdates(diff, config);
|
|
478
478
|
fromSource.push(...newFKs);
|
|
479
479
|
}
|
|
480
480
|
return fromSource;
|
|
@@ -8,7 +8,7 @@ import type { DatabasePlatform, ExistingTableInfo, SchemaDiff, SoftFKEntry, Sour
|
|
|
8
8
|
*/
|
|
9
9
|
export declare class SchemaEvolution {
|
|
10
10
|
private readonly DDL;
|
|
11
|
-
private readonly
|
|
11
|
+
private readonly mapper;
|
|
12
12
|
/**
|
|
13
13
|
* Diff a source object against an existing target table.
|
|
14
14
|
* Returns added, modified, and removed columns.
|
|
@@ -19,7 +19,7 @@ export declare class SchemaEvolution {
|
|
|
19
19
|
* as ALTER-ADD configs. New tables get these in CREATE TABLE (DDLGenerator.StandardColumns);
|
|
20
20
|
* existing tables created before a column was added get it ALTER-ed in here.
|
|
21
21
|
*/
|
|
22
|
-
private
|
|
22
|
+
private ensureStandardColumns;
|
|
23
23
|
/**
|
|
24
24
|
* Generate ALTER TABLE SQL for a schema diff.
|
|
25
25
|
*/
|
|
@@ -28,6 +28,6 @@ export declare class SchemaEvolution {
|
|
|
28
28
|
* Identify new soft FK entries from a diff (for added FK columns).
|
|
29
29
|
*/
|
|
30
30
|
GenerateEvolutionSoftFKUpdates(diff: SchemaDiff, targetConfig: TargetTableConfig): SoftFKEntry[];
|
|
31
|
-
private
|
|
31
|
+
private hasColumnChanged;
|
|
32
32
|
}
|
|
33
33
|
//# sourceMappingURL=SchemaEvolution.d.ts.map
|
package/dist/SchemaEvolution.js
CHANGED
|
@@ -6,7 +6,7 @@ import { TypeMapper } from './TypeMapper.js';
|
|
|
6
6
|
export class SchemaEvolution {
|
|
7
7
|
constructor() {
|
|
8
8
|
this.DDL = new DDLGenerator();
|
|
9
|
-
this.
|
|
9
|
+
this.mapper = new TypeMapper();
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
12
|
* Diff a source object against an existing target table.
|
|
@@ -39,7 +39,7 @@ export class SchemaEvolution {
|
|
|
39
39
|
if (pkNames.has(lowerName)) {
|
|
40
40
|
if (existingNames.has(lowerName)) {
|
|
41
41
|
const existingCol = existing.Columns.find(c => c.Name.toLowerCase() === lowerName);
|
|
42
|
-
if (existingCol && this.
|
|
42
|
+
if (existingCol && this.hasColumnChanged(existingCol, col)) {
|
|
43
43
|
warnings.push(`PK column '${col.TargetColumnName}' type changed from ${existingCol.SqlType} to ${col.TargetSqlType} — skipped (requires manual constraint drop/recreate)`);
|
|
44
44
|
}
|
|
45
45
|
}
|
|
@@ -50,7 +50,7 @@ export class SchemaEvolution {
|
|
|
50
50
|
}
|
|
51
51
|
else {
|
|
52
52
|
const existingCol = existing.Columns.find(c => c.Name.toLowerCase() === lowerName);
|
|
53
|
-
if (existingCol && this.
|
|
53
|
+
if (existingCol && this.hasColumnChanged(existingCol, col)) {
|
|
54
54
|
modified.push({
|
|
55
55
|
ColumnName: col.TargetColumnName,
|
|
56
56
|
OldType: existingCol.SqlType,
|
|
@@ -74,7 +74,7 @@ export class SchemaEvolution {
|
|
|
74
74
|
// skips standard columns (they're in `standardCols`), so a table created before a standard
|
|
75
75
|
// column was introduced would never get it. ALTER-add any that are missing — otherwise the
|
|
76
76
|
// engine's writes to a missing column silently no-op (hasField guard).
|
|
77
|
-
added.push(...this.
|
|
77
|
+
added.push(...this.ensureStandardColumns(existing, platform));
|
|
78
78
|
return { AddedColumns: added, ModifiedColumns: modified, RemovedColumns: removed, Warnings: warnings };
|
|
79
79
|
}
|
|
80
80
|
/**
|
|
@@ -82,7 +82,7 @@ export class SchemaEvolution {
|
|
|
82
82
|
* as ALTER-ADD configs. New tables get these in CREATE TABLE (DDLGenerator.StandardColumns);
|
|
83
83
|
* existing tables created before a column was added get it ALTER-ed in here.
|
|
84
84
|
*/
|
|
85
|
-
|
|
85
|
+
ensureStandardColumns(existing, platform) {
|
|
86
86
|
const have = new Set(existing.Columns.map(c => c.Name.toLowerCase()));
|
|
87
87
|
const isSql = platform === 'sqlserver';
|
|
88
88
|
const std = [
|
|
@@ -126,7 +126,7 @@ export class SchemaEvolution {
|
|
|
126
126
|
const addedNames = new Set(diff.AddedColumns.map(c => c.TargetColumnName));
|
|
127
127
|
return targetConfig.SoftForeignKeys.filter(fk => addedNames.has(fk.FieldName));
|
|
128
128
|
}
|
|
129
|
-
|
|
129
|
+
hasColumnChanged(existing, target) {
|
|
130
130
|
// Normalize types for comparison (case-insensitive, strip whitespace)
|
|
131
131
|
const normalizedExisting = existing.SqlType.toLowerCase().replace(/\s+/g, '');
|
|
132
132
|
const normalizedTarget = target.TargetSqlType.toLowerCase().replace(/\s+/g, '');
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memberjunction/integration-schema-builder",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "6.2.0-edge.
|
|
4
|
+
"version": "6.2.0-edge.1",
|
|
5
5
|
"description": "MemberJunction Integration Schema Builder - file-emission-only DDL generation for integration tables",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
"/dist"
|
|
10
10
|
],
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@memberjunction/core": "6.2.0-edge.
|
|
13
|
-
"@memberjunction/global": "6.2.0-edge.
|
|
14
|
-
"@memberjunction/integration-engine": "6.2.0-edge.
|
|
15
|
-
"@memberjunction/schema-engine": "6.2.0-edge.
|
|
16
|
-
"@memberjunction/sql-dialect": "6.2.0-edge.
|
|
12
|
+
"@memberjunction/core": "6.2.0-edge.1",
|
|
13
|
+
"@memberjunction/global": "6.2.0-edge.1",
|
|
14
|
+
"@memberjunction/integration-engine": "6.2.0-edge.1",
|
|
15
|
+
"@memberjunction/schema-engine": "6.2.0-edge.1",
|
|
16
|
+
"@memberjunction/sql-dialect": "6.2.0-edge.1"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"typescript": "^5.9.3",
|