@peter.prib/renderengine 0.2.10 → 0.2.11
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/REFERENCE.md +6 -0
- package/dist/renderengine-workspace.umd.js +42 -18
- package/dist/renderengine.umd.js +42 -18
- package/package.json +1 -1
- package/src/charting/datamodel/DataModelChecker.js +1 -5
- package/src/charting/datamodel/LevelSchema.js +23 -0
- package/src/charting/datamodel/SqlDdl.js +18 -13
package/REFERENCE.md
CHANGED
|
@@ -875,6 +875,12 @@ types — see `ARCHITECTURE.md`):
|
|
|
875
875
|
a comment where one cannot be made; `COMMENT ON`s for definitions (`comments: false` leaves them out). Names SQL
|
|
876
876
|
would not take bare are quoted. ANSI SQL as PostgreSQL reads it.
|
|
877
877
|
|
|
878
|
+
A table's name may be schema-qualified (`sales.customer`, as a database lists its tables): the DDL names it in its
|
|
879
|
+
schema (`sales.customer`, each part quoted as it needs, `SqlDdl.tableName()`), and a reference to it is
|
|
880
|
+
`sales.customer` or `sales.customer.id`. The check and the DDL read a reference the one way,
|
|
881
|
+
`LevelSchema.resolveReference(ref)`: the whole text as a table (by id or name, any case), else what is before its
|
|
882
|
+
last dot as the table and the rest as its column — `{ entity, column }`, or null.
|
|
883
|
+
|
|
878
884
|
**A dimensional scheme.** At the logical and physical levels an entity may have a role (`EntityRole`, registered as
|
|
879
885
|
relationship kinds are): a **fact** (`FactRole`, `role: 'fact'`: what is measured, at a stated `grain`, its
|
|
880
886
|
attributes' `measure` additive, semi-additive or non-additive) or a **dimension** (`DimensionRole`: its `scdType`,
|
|
@@ -44885,6 +44885,29 @@
|
|
|
44885
44885
|
return this.entities.find(e => e.id === id) ?? null;
|
|
44886
44886
|
}
|
|
44887
44887
|
|
|
44888
|
+
/** An entity by id, else by name (any case), or null. */
|
|
44889
|
+
named(text) {
|
|
44890
|
+
const s = String(text ?? '').trim();
|
|
44891
|
+
return this.entity(s) ?? this.entities.find(e => String(e.name ?? '').toLowerCase() === s.toLowerCase()) ?? null;
|
|
44892
|
+
}
|
|
44893
|
+
|
|
44894
|
+
/**
|
|
44895
|
+
* What a foreign key's `references` names: `{ entity, column }` (column
|
|
44896
|
+
* null when it names the table alone), or null. `table` or `table.column`,
|
|
44897
|
+
* the table by id or name; a table's own name may hold dots (a
|
|
44898
|
+
* schema-qualified `sales.customer`), so the whole text is tried as a
|
|
44899
|
+
* table first, then what is before its last dot, the rest its column.
|
|
44900
|
+
*/
|
|
44901
|
+
resolveReference(ref) {
|
|
44902
|
+
const text = String(ref ?? '').trim();
|
|
44903
|
+
if (!text) return null;
|
|
44904
|
+
const whole = this.named(text);
|
|
44905
|
+
if (whole) return { entity: whole, column: null };
|
|
44906
|
+
const dot = text.lastIndexOf('.');
|
|
44907
|
+
const entity = dot > 0 ? this.named(text.slice(0, dot)) : null;
|
|
44908
|
+
return entity ? { entity, column: text.slice(dot + 1).trim() || null } : null;
|
|
44909
|
+
}
|
|
44910
|
+
|
|
44888
44911
|
/** Add an entity (its id unique at this level). */
|
|
44889
44912
|
addEntity(entity) {
|
|
44890
44913
|
if (!entity?.id) throw new TypeError('An entity needs an id');
|
|
@@ -45325,17 +45348,13 @@
|
|
|
45325
45348
|
foreignKeys() {
|
|
45326
45349
|
return ATTRIBUTED.flatMap(level => {
|
|
45327
45350
|
const schema = this.model.level(level);
|
|
45328
|
-
const find = (ref) => {
|
|
45329
|
-
const [name] = String(ref).split('.');
|
|
45330
|
-
return schema.entity(name) ?? schema.entities.find(e => e.name.toLowerCase() === name.trim().toLowerCase()) ?? null;
|
|
45331
|
-
};
|
|
45332
45351
|
const related = (a, b) => schema.relationships.some(r => (r.source === a && r.target === b) || (r.source === b && r.target === a));
|
|
45333
45352
|
return schema.entities.flatMap(e => (e.attributes ?? []).filter(a => a.isFK).flatMap(a => {
|
|
45334
45353
|
if (!a.references) {
|
|
45335
45354
|
return schema.relationships.some(r => r.source === e.id || r.target === e.id) ? []
|
|
45336
45355
|
: [this._entity(level, e, 'foreign-key-unrelated', 'info', `"${e.name}.${a.name}" is a foreign key, but "${e.name}" has no relationship`)];
|
|
45337
45356
|
}
|
|
45338
|
-
const target =
|
|
45357
|
+
const target = schema.resolveReference(a.references)?.entity;
|
|
45339
45358
|
if (!target) return [this._entity(level, e, 'reference-missing', 'warning', `"${e.name}.${a.name}" references "${a.references}", which is not at the ${level} level`)];
|
|
45340
45359
|
return related(e.id, target.id) ? []
|
|
45341
45360
|
: [this._entity(level, e, 'foreign-key-without-relationship', 'warning', `"${e.name}.${a.name}" references "${target.name}", but they have no relationship`)];
|
|
@@ -48277,6 +48296,15 @@
|
|
|
48277
48296
|
return /^[a-z_][a-z0-9_]*$/.test(s) && !RESERVED.has(s) ? s : `"${s.replace(/"/g, '""')}"`;
|
|
48278
48297
|
}
|
|
48279
48298
|
|
|
48299
|
+
/**
|
|
48300
|
+
* A table's name as SQL takes it: a schema-qualified one (`sales.customer`)
|
|
48301
|
+
* each part as name() has it, so it names the table in its schema rather
|
|
48302
|
+
* than one table called "sales.customer".
|
|
48303
|
+
*/
|
|
48304
|
+
static tableName(text) {
|
|
48305
|
+
return String(text).split('.').map(SqlDdl.name).join('.');
|
|
48306
|
+
}
|
|
48307
|
+
|
|
48280
48308
|
/** A text as an SQL string literal. */
|
|
48281
48309
|
static literal(text) {
|
|
48282
48310
|
return `'${String(text).replace(/'/g, "''")}'`;
|
|
@@ -48286,11 +48314,7 @@
|
|
|
48286
48314
|
write(model) {
|
|
48287
48315
|
const schema = model.level('physical');
|
|
48288
48316
|
const tables = schema.entities;
|
|
48289
|
-
const
|
|
48290
|
-
const [table, column] = String(ref).split('.').map(s => s.trim());
|
|
48291
|
-
const target = schema.entity(table) ?? tables.find(t => t.name.toLowerCase() === table.toLowerCase());
|
|
48292
|
-
return target ? { target, column } : null;
|
|
48293
|
-
};
|
|
48317
|
+
const table = SqlDdl.tableName;
|
|
48294
48318
|
const out = [`-- ${model.name || 'Data model'}: the physical level, ${tables.length} table${tables.length === 1 ? '' : 's'}`, ''];
|
|
48295
48319
|
for (const t of tables) {
|
|
48296
48320
|
const columns = t.attributes ?? [];
|
|
@@ -48300,28 +48324,28 @@
|
|
|
48300
48324
|
...(keys.length ? [` PRIMARY KEY (${keys.join(', ')})`] : []),
|
|
48301
48325
|
...columns.filter(c => c.isUnique && !c.isPK).map(c => ` UNIQUE (${SqlDdl.name(c.name)})`),
|
|
48302
48326
|
];
|
|
48303
|
-
out.push(`CREATE TABLE ${
|
|
48327
|
+
out.push(`CREATE TABLE ${table(t.name)} (`, lines.join(',\n'), ');', '');
|
|
48304
48328
|
}
|
|
48305
48329
|
for (const t of tables) {
|
|
48306
48330
|
for (const c of (t.attributes ?? []).filter(a => a.isFK && a.references)) {
|
|
48307
|
-
const ref =
|
|
48331
|
+
const ref = schema.resolveReference(c.references);
|
|
48308
48332
|
if (!ref) {
|
|
48309
48333
|
out.push(`-- ${t.name}.${c.name} references ${c.references}, which is not a table here`);
|
|
48310
48334
|
continue;
|
|
48311
48335
|
}
|
|
48312
|
-
const theirs = ref.column ? [ref.column] : (ref.
|
|
48336
|
+
const theirs = ref.column ? [ref.column] : (ref.entity.attributes ?? []).filter(a => a.isPK).map(a => a.name);
|
|
48313
48337
|
if (!theirs.length) {
|
|
48314
|
-
out.push(`-- ${t.name}.${c.name} references ${ref.
|
|
48338
|
+
out.push(`-- ${t.name}.${c.name} references ${ref.entity.name}, which has no primary key`);
|
|
48315
48339
|
continue;
|
|
48316
48340
|
}
|
|
48317
|
-
out.push(`ALTER TABLE ${
|
|
48318
|
-
+ ` FOREIGN KEY (${SqlDdl.name(c.name)}) REFERENCES ${
|
|
48341
|
+
out.push(`ALTER TABLE ${table(t.name)} ADD CONSTRAINT ${SqlDdl.name(`fk_${t.name}_${c.name}`.toLowerCase().replace(/[^a-z0-9_]+/g, '_'))}`
|
|
48342
|
+
+ ` FOREIGN KEY (${SqlDdl.name(c.name)}) REFERENCES ${table(ref.entity.name)} (${theirs.map(SqlDdl.name).join(', ')});`);
|
|
48319
48343
|
}
|
|
48320
48344
|
}
|
|
48321
48345
|
if (this.comments) {
|
|
48322
48346
|
const notes = tables.flatMap(t => [
|
|
48323
|
-
...(t.description ? [`COMMENT ON TABLE ${
|
|
48324
|
-
...(t.attributes ?? []).filter(c => c.description).map(c => `COMMENT ON COLUMN ${
|
|
48347
|
+
...(t.description ? [`COMMENT ON TABLE ${table(t.name)} IS ${SqlDdl.literal(t.description)};`] : []),
|
|
48348
|
+
...(t.attributes ?? []).filter(c => c.description).map(c => `COMMENT ON COLUMN ${table(t.name)}.${SqlDdl.name(c.name)} IS ${SqlDdl.literal(c.description)};`),
|
|
48325
48349
|
]);
|
|
48326
48350
|
if (notes.length) out.push('', ...notes);
|
|
48327
48351
|
}
|
package/dist/renderengine.umd.js
CHANGED
|
@@ -41279,6 +41279,29 @@
|
|
|
41279
41279
|
return this.entities.find(e => e.id === id) ?? null;
|
|
41280
41280
|
}
|
|
41281
41281
|
|
|
41282
|
+
/** An entity by id, else by name (any case), or null. */
|
|
41283
|
+
named(text) {
|
|
41284
|
+
const s = String(text ?? '').trim();
|
|
41285
|
+
return this.entity(s) ?? this.entities.find(e => String(e.name ?? '').toLowerCase() === s.toLowerCase()) ?? null;
|
|
41286
|
+
}
|
|
41287
|
+
|
|
41288
|
+
/**
|
|
41289
|
+
* What a foreign key's `references` names: `{ entity, column }` (column
|
|
41290
|
+
* null when it names the table alone), or null. `table` or `table.column`,
|
|
41291
|
+
* the table by id or name; a table's own name may hold dots (a
|
|
41292
|
+
* schema-qualified `sales.customer`), so the whole text is tried as a
|
|
41293
|
+
* table first, then what is before its last dot, the rest its column.
|
|
41294
|
+
*/
|
|
41295
|
+
resolveReference(ref) {
|
|
41296
|
+
const text = String(ref ?? '').trim();
|
|
41297
|
+
if (!text) return null;
|
|
41298
|
+
const whole = this.named(text);
|
|
41299
|
+
if (whole) return { entity: whole, column: null };
|
|
41300
|
+
const dot = text.lastIndexOf('.');
|
|
41301
|
+
const entity = dot > 0 ? this.named(text.slice(0, dot)) : null;
|
|
41302
|
+
return entity ? { entity, column: text.slice(dot + 1).trim() || null } : null;
|
|
41303
|
+
}
|
|
41304
|
+
|
|
41282
41305
|
/** Add an entity (its id unique at this level). */
|
|
41283
41306
|
addEntity(entity) {
|
|
41284
41307
|
if (!entity?.id) throw new TypeError('An entity needs an id');
|
|
@@ -41719,17 +41742,13 @@
|
|
|
41719
41742
|
foreignKeys() {
|
|
41720
41743
|
return ATTRIBUTED.flatMap(level => {
|
|
41721
41744
|
const schema = this.model.level(level);
|
|
41722
|
-
const find = (ref) => {
|
|
41723
|
-
const [name] = String(ref).split('.');
|
|
41724
|
-
return schema.entity(name) ?? schema.entities.find(e => e.name.toLowerCase() === name.trim().toLowerCase()) ?? null;
|
|
41725
|
-
};
|
|
41726
41745
|
const related = (a, b) => schema.relationships.some(r => (r.source === a && r.target === b) || (r.source === b && r.target === a));
|
|
41727
41746
|
return schema.entities.flatMap(e => (e.attributes ?? []).filter(a => a.isFK).flatMap(a => {
|
|
41728
41747
|
if (!a.references) {
|
|
41729
41748
|
return schema.relationships.some(r => r.source === e.id || r.target === e.id) ? []
|
|
41730
41749
|
: [this._entity(level, e, 'foreign-key-unrelated', 'info', `"${e.name}.${a.name}" is a foreign key, but "${e.name}" has no relationship`)];
|
|
41731
41750
|
}
|
|
41732
|
-
const target =
|
|
41751
|
+
const target = schema.resolveReference(a.references)?.entity;
|
|
41733
41752
|
if (!target) return [this._entity(level, e, 'reference-missing', 'warning', `"${e.name}.${a.name}" references "${a.references}", which is not at the ${level} level`)];
|
|
41734
41753
|
return related(e.id, target.id) ? []
|
|
41735
41754
|
: [this._entity(level, e, 'foreign-key-without-relationship', 'warning', `"${e.name}.${a.name}" references "${target.name}", but they have no relationship`)];
|
|
@@ -44671,6 +44690,15 @@
|
|
|
44671
44690
|
return /^[a-z_][a-z0-9_]*$/.test(s) && !RESERVED.has(s) ? s : `"${s.replace(/"/g, '""')}"`;
|
|
44672
44691
|
}
|
|
44673
44692
|
|
|
44693
|
+
/**
|
|
44694
|
+
* A table's name as SQL takes it: a schema-qualified one (`sales.customer`)
|
|
44695
|
+
* each part as name() has it, so it names the table in its schema rather
|
|
44696
|
+
* than one table called "sales.customer".
|
|
44697
|
+
*/
|
|
44698
|
+
static tableName(text) {
|
|
44699
|
+
return String(text).split('.').map(SqlDdl.name).join('.');
|
|
44700
|
+
}
|
|
44701
|
+
|
|
44674
44702
|
/** A text as an SQL string literal. */
|
|
44675
44703
|
static literal(text) {
|
|
44676
44704
|
return `'${String(text).replace(/'/g, "''")}'`;
|
|
@@ -44680,11 +44708,7 @@
|
|
|
44680
44708
|
write(model) {
|
|
44681
44709
|
const schema = model.level('physical');
|
|
44682
44710
|
const tables = schema.entities;
|
|
44683
|
-
const
|
|
44684
|
-
const [table, column] = String(ref).split('.').map(s => s.trim());
|
|
44685
|
-
const target = schema.entity(table) ?? tables.find(t => t.name.toLowerCase() === table.toLowerCase());
|
|
44686
|
-
return target ? { target, column } : null;
|
|
44687
|
-
};
|
|
44711
|
+
const table = SqlDdl.tableName;
|
|
44688
44712
|
const out = [`-- ${model.name || 'Data model'}: the physical level, ${tables.length} table${tables.length === 1 ? '' : 's'}`, ''];
|
|
44689
44713
|
for (const t of tables) {
|
|
44690
44714
|
const columns = t.attributes ?? [];
|
|
@@ -44694,28 +44718,28 @@
|
|
|
44694
44718
|
...(keys.length ? [` PRIMARY KEY (${keys.join(', ')})`] : []),
|
|
44695
44719
|
...columns.filter(c => c.isUnique && !c.isPK).map(c => ` UNIQUE (${SqlDdl.name(c.name)})`),
|
|
44696
44720
|
];
|
|
44697
|
-
out.push(`CREATE TABLE ${
|
|
44721
|
+
out.push(`CREATE TABLE ${table(t.name)} (`, lines.join(',\n'), ');', '');
|
|
44698
44722
|
}
|
|
44699
44723
|
for (const t of tables) {
|
|
44700
44724
|
for (const c of (t.attributes ?? []).filter(a => a.isFK && a.references)) {
|
|
44701
|
-
const ref =
|
|
44725
|
+
const ref = schema.resolveReference(c.references);
|
|
44702
44726
|
if (!ref) {
|
|
44703
44727
|
out.push(`-- ${t.name}.${c.name} references ${c.references}, which is not a table here`);
|
|
44704
44728
|
continue;
|
|
44705
44729
|
}
|
|
44706
|
-
const theirs = ref.column ? [ref.column] : (ref.
|
|
44730
|
+
const theirs = ref.column ? [ref.column] : (ref.entity.attributes ?? []).filter(a => a.isPK).map(a => a.name);
|
|
44707
44731
|
if (!theirs.length) {
|
|
44708
|
-
out.push(`-- ${t.name}.${c.name} references ${ref.
|
|
44732
|
+
out.push(`-- ${t.name}.${c.name} references ${ref.entity.name}, which has no primary key`);
|
|
44709
44733
|
continue;
|
|
44710
44734
|
}
|
|
44711
|
-
out.push(`ALTER TABLE ${
|
|
44712
|
-
+ ` FOREIGN KEY (${SqlDdl.name(c.name)}) REFERENCES ${
|
|
44735
|
+
out.push(`ALTER TABLE ${table(t.name)} ADD CONSTRAINT ${SqlDdl.name(`fk_${t.name}_${c.name}`.toLowerCase().replace(/[^a-z0-9_]+/g, '_'))}`
|
|
44736
|
+
+ ` FOREIGN KEY (${SqlDdl.name(c.name)}) REFERENCES ${table(ref.entity.name)} (${theirs.map(SqlDdl.name).join(', ')});`);
|
|
44713
44737
|
}
|
|
44714
44738
|
}
|
|
44715
44739
|
if (this.comments) {
|
|
44716
44740
|
const notes = tables.flatMap(t => [
|
|
44717
|
-
...(t.description ? [`COMMENT ON TABLE ${
|
|
44718
|
-
...(t.attributes ?? []).filter(c => c.description).map(c => `COMMENT ON COLUMN ${
|
|
44741
|
+
...(t.description ? [`COMMENT ON TABLE ${table(t.name)} IS ${SqlDdl.literal(t.description)};`] : []),
|
|
44742
|
+
...(t.attributes ?? []).filter(c => c.description).map(c => `COMMENT ON COLUMN ${table(t.name)}.${SqlDdl.name(c.name)} IS ${SqlDdl.literal(c.description)};`),
|
|
44719
44743
|
]);
|
|
44720
44744
|
if (notes.length) out.push('', ...notes);
|
|
44721
44745
|
}
|
package/package.json
CHANGED
|
@@ -53,17 +53,13 @@ export class DataModelChecker extends ModelChecker {
|
|
|
53
53
|
foreignKeys() {
|
|
54
54
|
return ATTRIBUTED.flatMap(level => {
|
|
55
55
|
const schema = this.model.level(level);
|
|
56
|
-
const find = (ref) => {
|
|
57
|
-
const [name] = String(ref).split('.');
|
|
58
|
-
return schema.entity(name) ?? schema.entities.find(e => e.name.toLowerCase() === name.trim().toLowerCase()) ?? null;
|
|
59
|
-
};
|
|
60
56
|
const related = (a, b) => schema.relationships.some(r => (r.source === a && r.target === b) || (r.source === b && r.target === a));
|
|
61
57
|
return schema.entities.flatMap(e => (e.attributes ?? []).filter(a => a.isFK).flatMap(a => {
|
|
62
58
|
if (!a.references) {
|
|
63
59
|
return schema.relationships.some(r => r.source === e.id || r.target === e.id) ? []
|
|
64
60
|
: [this._entity(level, e, 'foreign-key-unrelated', 'info', `"${e.name}.${a.name}" is a foreign key, but "${e.name}" has no relationship`)];
|
|
65
61
|
}
|
|
66
|
-
const target =
|
|
62
|
+
const target = schema.resolveReference(a.references)?.entity;
|
|
67
63
|
if (!target) return [this._entity(level, e, 'reference-missing', 'warning', `"${e.name}.${a.name}" references "${a.references}", which is not at the ${level} level`)];
|
|
68
64
|
return related(e.id, target.id) ? []
|
|
69
65
|
: [this._entity(level, e, 'foreign-key-without-relationship', 'warning', `"${e.name}.${a.name}" references "${target.name}", but they have no relationship`)];
|
|
@@ -27,6 +27,29 @@ export class LevelSchema {
|
|
|
27
27
|
return this.entities.find(e => e.id === id) ?? null;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/** An entity by id, else by name (any case), or null. */
|
|
31
|
+
named(text) {
|
|
32
|
+
const s = String(text ?? '').trim();
|
|
33
|
+
return this.entity(s) ?? this.entities.find(e => String(e.name ?? '').toLowerCase() === s.toLowerCase()) ?? null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* What a foreign key's `references` names: `{ entity, column }` (column
|
|
38
|
+
* null when it names the table alone), or null. `table` or `table.column`,
|
|
39
|
+
* the table by id or name; a table's own name may hold dots (a
|
|
40
|
+
* schema-qualified `sales.customer`), so the whole text is tried as a
|
|
41
|
+
* table first, then what is before its last dot, the rest its column.
|
|
42
|
+
*/
|
|
43
|
+
resolveReference(ref) {
|
|
44
|
+
const text = String(ref ?? '').trim();
|
|
45
|
+
if (!text) return null;
|
|
46
|
+
const whole = this.named(text);
|
|
47
|
+
if (whole) return { entity: whole, column: null };
|
|
48
|
+
const dot = text.lastIndexOf('.');
|
|
49
|
+
const entity = dot > 0 ? this.named(text.slice(0, dot)) : null;
|
|
50
|
+
return entity ? { entity, column: text.slice(dot + 1).trim() || null } : null;
|
|
51
|
+
}
|
|
52
|
+
|
|
30
53
|
/** Add an entity (its id unique at this level). */
|
|
31
54
|
addEntity(entity) {
|
|
32
55
|
if (!entity?.id) throw new TypeError('An entity needs an id');
|
|
@@ -27,6 +27,15 @@ export class SqlDdl {
|
|
|
27
27
|
return /^[a-z_][a-z0-9_]*$/.test(s) && !RESERVED.has(s) ? s : `"${s.replace(/"/g, '""')}"`;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* A table's name as SQL takes it: a schema-qualified one (`sales.customer`)
|
|
32
|
+
* each part as name() has it, so it names the table in its schema rather
|
|
33
|
+
* than one table called "sales.customer".
|
|
34
|
+
*/
|
|
35
|
+
static tableName(text) {
|
|
36
|
+
return String(text).split('.').map(SqlDdl.name).join('.');
|
|
37
|
+
}
|
|
38
|
+
|
|
30
39
|
/** A text as an SQL string literal. */
|
|
31
40
|
static literal(text) {
|
|
32
41
|
return `'${String(text).replace(/'/g, "''")}'`;
|
|
@@ -36,11 +45,7 @@ export class SqlDdl {
|
|
|
36
45
|
write(model) {
|
|
37
46
|
const schema = model.level('physical');
|
|
38
47
|
const tables = schema.entities;
|
|
39
|
-
const
|
|
40
|
-
const [table, column] = String(ref).split('.').map(s => s.trim());
|
|
41
|
-
const target = schema.entity(table) ?? tables.find(t => t.name.toLowerCase() === table.toLowerCase());
|
|
42
|
-
return target ? { target, column } : null;
|
|
43
|
-
};
|
|
48
|
+
const table = SqlDdl.tableName;
|
|
44
49
|
const out = [`-- ${model.name || 'Data model'}: the physical level, ${tables.length} table${tables.length === 1 ? '' : 's'}`, ''];
|
|
45
50
|
for (const t of tables) {
|
|
46
51
|
const columns = t.attributes ?? [];
|
|
@@ -50,28 +55,28 @@ export class SqlDdl {
|
|
|
50
55
|
...(keys.length ? [` PRIMARY KEY (${keys.join(', ')})`] : []),
|
|
51
56
|
...columns.filter(c => c.isUnique && !c.isPK).map(c => ` UNIQUE (${SqlDdl.name(c.name)})`),
|
|
52
57
|
];
|
|
53
|
-
out.push(`CREATE TABLE ${
|
|
58
|
+
out.push(`CREATE TABLE ${table(t.name)} (`, lines.join(',\n'), ');', '');
|
|
54
59
|
}
|
|
55
60
|
for (const t of tables) {
|
|
56
61
|
for (const c of (t.attributes ?? []).filter(a => a.isFK && a.references)) {
|
|
57
|
-
const ref =
|
|
62
|
+
const ref = schema.resolveReference(c.references);
|
|
58
63
|
if (!ref) {
|
|
59
64
|
out.push(`-- ${t.name}.${c.name} references ${c.references}, which is not a table here`);
|
|
60
65
|
continue;
|
|
61
66
|
}
|
|
62
|
-
const theirs = ref.column ? [ref.column] : (ref.
|
|
67
|
+
const theirs = ref.column ? [ref.column] : (ref.entity.attributes ?? []).filter(a => a.isPK).map(a => a.name);
|
|
63
68
|
if (!theirs.length) {
|
|
64
|
-
out.push(`-- ${t.name}.${c.name} references ${ref.
|
|
69
|
+
out.push(`-- ${t.name}.${c.name} references ${ref.entity.name}, which has no primary key`);
|
|
65
70
|
continue;
|
|
66
71
|
}
|
|
67
|
-
out.push(`ALTER TABLE ${
|
|
68
|
-
+ ` FOREIGN KEY (${SqlDdl.name(c.name)}) REFERENCES ${
|
|
72
|
+
out.push(`ALTER TABLE ${table(t.name)} ADD CONSTRAINT ${SqlDdl.name(`fk_${t.name}_${c.name}`.toLowerCase().replace(/[^a-z0-9_]+/g, '_'))}`
|
|
73
|
+
+ ` FOREIGN KEY (${SqlDdl.name(c.name)}) REFERENCES ${table(ref.entity.name)} (${theirs.map(SqlDdl.name).join(', ')});`);
|
|
69
74
|
}
|
|
70
75
|
}
|
|
71
76
|
if (this.comments) {
|
|
72
77
|
const notes = tables.flatMap(t => [
|
|
73
|
-
...(t.description ? [`COMMENT ON TABLE ${
|
|
74
|
-
...(t.attributes ?? []).filter(c => c.description).map(c => `COMMENT ON COLUMN ${
|
|
78
|
+
...(t.description ? [`COMMENT ON TABLE ${table(t.name)} IS ${SqlDdl.literal(t.description)};`] : []),
|
|
79
|
+
...(t.attributes ?? []).filter(c => c.description).map(c => `COMMENT ON COLUMN ${table(t.name)}.${SqlDdl.name(c.name)} IS ${SqlDdl.literal(c.description)};`),
|
|
75
80
|
]);
|
|
76
81
|
if (notes.length) out.push('', ...notes);
|
|
77
82
|
}
|