@adecore/database 0.39.0 → 0.40.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/console/QueryConsole.js +2 -10
- package/dist/console/SchemaPicker.js +0 -1
- package/dist/console/StatementResultView.js +1 -1
- package/dist/ddl/diff.js +1 -1
- package/dist/explorer/tree.js +0 -1
- package/dist/export-formats.d.ts +2 -0
- package/dist/export-formats.js +1 -0
- package/dist/grid/CopyAsMenu.js +0 -1
- package/dist/grid/DataGrid.js +10 -10
- package/dist/grid/column-layout.d.ts +0 -1
- package/dist/grid/column-layout.js +1 -2
- package/dist/grid/column-view.js +3 -2
- package/dist/grid/display.js +1 -2
- package/dist/host/host.js +1 -2
- package/dist/host/validate.d.ts +3 -0
- package/dist/host/validate.js +18 -15
- package/dist/same-list.d.ts +1 -0
- package/dist/same-list.js +1 -0
- package/dist/stored-size.d.ts +1 -0
- package/dist/stored-size.js +10 -0
- package/dist/table/TableToolbar.js +1 -1
- package/dist/table/TableView.js +2 -10
- package/package.json +15 -15
- package/src/DatabaseProvider.tsx +2 -0
- package/src/KeyIcon.tsx +1 -0
- package/src/client/client.ts +13 -0
- package/src/client-context.ts +2 -0
- package/src/connections/ConnectionForm.tsx +11 -0
- package/src/connections/ConnectionManager.tsx +9 -0
- package/src/connections/connection-config.ts +21 -0
- package/src/connections/docker.ts +2 -0
- package/src/connections/use-docker-containers.ts +3 -0
- package/src/console/DestructiveDialog.tsx +1 -0
- package/src/console/QueryConsole.tsx +38 -10
- package/src/console/SchemaPicker.tsx +1 -1
- package/src/console/SqlEditor.tsx +10 -0
- package/src/console/StatementResultView.tsx +7 -2
- package/src/console/TransactionControls.tsx +1 -0
- package/src/console/destructive.ts +11 -0
- package/src/console/editing.ts +10 -0
- package/src/console/history.ts +8 -0
- package/src/console/labels.ts +1 -0
- package/src/console/outcome.ts +1 -0
- package/src/console/result-keys.ts +16 -0
- package/src/console/shortcuts.ts +2 -0
- package/src/ddl/alter-mysql.ts +16 -0
- package/src/ddl/alter-sqlite.ts +21 -0
- package/src/ddl/alter.ts +2 -0
- package/src/ddl/column.ts +11 -0
- package/src/ddl/create.ts +15 -0
- package/src/ddl/dialect.ts +4 -0
- package/src/ddl/diff.ts +22 -2
- package/src/ddl/draft.ts +1 -0
- package/src/ddl/parse.ts +31 -0
- package/src/ddl/types.ts +3 -0
- package/src/ddl/validate.ts +12 -0
- package/src/designer/ColumnsEditor.tsx +3 -0
- package/src/designer/DesignerView.tsx +2 -0
- package/src/designer/ForeignKeysEditor.tsx +4 -0
- package/src/designer/IndexesEditor.tsx +1 -0
- package/src/designer/NamePicker.tsx +2 -0
- package/src/designer/OptionsEditor.tsx +4 -0
- package/src/designer/SqlPreview.tsx +2 -0
- package/src/designer/TableDesigner.tsx +14 -0
- package/src/designer/edit.ts +13 -0
- package/src/designer/plan.ts +3 -0
- package/src/designer/recover.ts +1 -0
- package/src/designer/useReferenceColumns.ts +5 -0
- package/src/explorer/DatabaseExplorer.tsx +40 -0
- package/src/explorer/RowMenu.tsx +2 -0
- package/src/explorer/TableDialog.tsx +6 -0
- package/src/explorer/expansion.ts +8 -0
- package/src/explorer/tree.ts +73 -2
- package/src/explorer/use-explorer-loads.ts +15 -0
- package/src/explorer/use-stored-expansion.ts +4 -0
- package/src/export-formats.ts +3 -0
- package/src/grid/CellEditor.tsx +1 -0
- package/src/grid/CopyAsMenu.tsx +1 -1
- package/src/grid/DataGrid.tsx +93 -23
- package/src/grid/EnumPicker.tsx +6 -0
- package/src/grid/GridHeaderCell.tsx +11 -0
- package/src/grid/GridRowView.tsx +3 -0
- package/src/grid/RecordView.tsx +19 -0
- package/src/grid/ValueDock.tsx +2 -0
- package/src/grid/aggregates.ts +12 -0
- package/src/grid/column-layout.ts +12 -4
- package/src/grid/column-selection.ts +9 -0
- package/src/grid/column-view.ts +6 -2
- package/src/grid/copy-formats.ts +13 -0
- package/src/grid/display.ts +17 -3
- package/src/grid/edit-value.ts +11 -0
- package/src/grid/enum-type.ts +15 -0
- package/src/grid/focused-value.ts +4 -0
- package/src/grid/layout.ts +12 -0
- package/src/grid/navigation.ts +5 -0
- package/src/grid/row-selection.ts +7 -0
- package/src/grid/sort.ts +1 -0
- package/src/grid/whole-cells.ts +8 -0
- package/src/host/host.ts +16 -3
- package/src/host/spawn.ts +2 -0
- package/src/host/validate.ts +37 -15
- package/src/same-list.ts +2 -0
- package/src/sql-split.ts +28 -0
- package/src/sql.ts +20 -0
- package/src/stored-size.ts +10 -0
- package/src/structure/StructureView.tsx +18 -0
- package/src/table/CommandField.tsx +11 -0
- package/src/table/ImportDialog.tsx +4 -0
- package/src/table/TableStatusBar.tsx +1 -0
- package/src/table/TableToolbar.tsx +2 -2
- package/src/table/TableView.tsx +50 -10
- package/src/table/command-field.ts +40 -0
- package/src/table/grid-rows.ts +13 -0
- package/src/table/import-mapping.ts +5 -0
- package/src/table/layout-store.ts +7 -0
- package/src/table/paging.ts +1 -0
- package/src/table/pending.ts +18 -0
- package/src/table/references.ts +4 -0
- package/src/table/useLoaded.ts +3 -0
- package/src/table/useStoredLayout.ts +2 -0
- package/src/table/useTableTransfer.ts +17 -1
- package/src/testing/fake.ts +24 -0
- package/src/use-database-resources.ts +1 -0
- package/src/use-popup-press.ts +3 -0
- package/src/value/ValueEditor.tsx +6 -0
- package/src/value/draft.ts +7 -0
- package/src/value/hex.ts +8 -0
- package/src/value/json.ts +16 -0
- package/src/value/views.ts +14 -0
- package/src/workbench/DatabaseWorkbench.tsx +22 -0
- package/src/workbench/tabs.ts +34 -0
package/src/console/history.ts
CHANGED
|
@@ -18,7 +18,9 @@ const isEntry = (value: unknown): value is HistoryEntry => {
|
|
|
18
18
|
if (value === null || typeof value !== 'object') {
|
|
19
19
|
return false;
|
|
20
20
|
}
|
|
21
|
+
|
|
21
22
|
const entry = value as Record<string, unknown>;
|
|
23
|
+
|
|
22
24
|
return (
|
|
23
25
|
typeof entry.sql === 'string' &&
|
|
24
26
|
typeof entry.at === 'number' &&
|
|
@@ -33,8 +35,10 @@ export const parseHistory = (raw: string | null): HistoryEntry[] => {
|
|
|
33
35
|
if (raw === null) {
|
|
34
36
|
return [];
|
|
35
37
|
}
|
|
38
|
+
|
|
36
39
|
try {
|
|
37
40
|
const parsed: unknown = JSON.parse(raw);
|
|
41
|
+
|
|
38
42
|
return Array.isArray(parsed) ? parsed.filter(isEntry).slice(0, HISTORY_LIMIT) : [];
|
|
39
43
|
} catch {
|
|
40
44
|
return [];
|
|
@@ -46,17 +50,21 @@ export const serializeHistory = (entries: readonly HistoryEntry[]): string => JS
|
|
|
46
50
|
/* The list with the run in front. A run of the same SQL as the latest one replaces it. */
|
|
47
51
|
export const addEntry = (entries: readonly HistoryEntry[], entry: HistoryEntry): HistoryEntry[] => {
|
|
48
52
|
const rest = entries[0]?.sql.trim() === entry.sql.trim() ? entries.slice(1) : entries;
|
|
53
|
+
|
|
49
54
|
return [entry, ...rest].slice(0, HISTORY_LIMIT);
|
|
50
55
|
};
|
|
51
56
|
|
|
52
57
|
/* The entries whose SQL holds every word of the query, in any case. */
|
|
53
58
|
export const searchHistory = (entries: readonly HistoryEntry[], query: string): readonly HistoryEntry[] => {
|
|
54
59
|
const words = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
60
|
+
|
|
55
61
|
if (words.length === 0) {
|
|
56
62
|
return entries;
|
|
57
63
|
}
|
|
64
|
+
|
|
58
65
|
return entries.filter((entry) => {
|
|
59
66
|
const sql = entry.sql.toLowerCase();
|
|
67
|
+
|
|
60
68
|
return words.every((word) => sql.includes(word));
|
|
61
69
|
});
|
|
62
70
|
};
|
package/src/console/labels.ts
CHANGED
|
@@ -3,5 +3,6 @@ const LABEL_LENGTH = 32;
|
|
|
3
3
|
/* The tab of a statement: its SQL on one line, cut where it gets long. */
|
|
4
4
|
export const statementLabel = (sql: string): string => {
|
|
5
5
|
const line = sql.replace(/\s+/g, ' ').trim();
|
|
6
|
+
|
|
6
7
|
return line.length > LABEL_LENGTH ? `${line.slice(0, LABEL_LENGTH - 1).trimEnd()}…` : line;
|
|
7
8
|
};
|
package/src/console/outcome.ts
CHANGED
|
@@ -10,5 +10,6 @@ export const outcomeOf = (results: readonly StatementResult[]): { readonly ok: b
|
|
|
10
10
|
/* Whether the helper can wrap the statement to fetch a page of it: a read that starts with `SELECT` or `WITH`. */
|
|
11
11
|
export const isPageable = (sql: string, engine: Engine): boolean => {
|
|
12
12
|
const keyword = firstKeyword(sql, engine);
|
|
13
|
+
|
|
13
14
|
return keyword === 'SELECT' || keyword === 'WITH';
|
|
14
15
|
};
|
|
@@ -21,11 +21,13 @@ const tableKey = ({ schema, table }: TableName): string => `${schema}\u0000${tab
|
|
|
21
21
|
/* The tables the columns of a result come from, each once, in the order they first appear. */
|
|
22
22
|
export const sourceTablesOf = (columns: readonly ResultColumn[]): TableName[] => {
|
|
23
23
|
const tables = new Map<string, TableName>();
|
|
24
|
+
|
|
24
25
|
for (const { source } of columns) {
|
|
25
26
|
if (source !== undefined && !tables.has(tableKey(source))) {
|
|
26
27
|
tables.set(tableKey(source), { schema: source.schema, table: source.table });
|
|
27
28
|
}
|
|
28
29
|
}
|
|
30
|
+
|
|
29
31
|
return [...tables.values()];
|
|
30
32
|
};
|
|
31
33
|
|
|
@@ -36,18 +38,23 @@ export const sourceTablesOf = (columns: readonly ResultColumn[]): TableName[] =>
|
|
|
36
38
|
export const resultKeysOf = (engine: Engine, columns: readonly ResultColumn[], structures: ReadonlyMap<string, TableStructure>): ResultKeys => {
|
|
37
39
|
const structureOf = (column: ResultColumn | undefined): TableStructure | undefined =>
|
|
38
40
|
column?.source === undefined ? undefined : structures.get(tableKey(column.source));
|
|
41
|
+
|
|
39
42
|
const foreignKeyAt = (column: number) => {
|
|
40
43
|
const source = columns[column]?.source;
|
|
41
44
|
const structure = structureOf(columns[column]);
|
|
45
|
+
|
|
42
46
|
return source === undefined || structure === undefined ? undefined : foreignKeyOf(structure.foreignKeys, source.column);
|
|
43
47
|
};
|
|
48
|
+
|
|
44
49
|
return {
|
|
45
50
|
columns: columns.map((column): GridColumn => {
|
|
46
51
|
const structure = structureOf(column);
|
|
47
52
|
const name = column.source?.column;
|
|
53
|
+
|
|
48
54
|
if (structure === undefined || name === undefined) {
|
|
49
55
|
return column;
|
|
50
56
|
}
|
|
57
|
+
|
|
51
58
|
return {
|
|
52
59
|
...column,
|
|
53
60
|
primaryKey: structure.primaryKey.includes(name),
|
|
@@ -58,14 +65,17 @@ export const resultKeysOf = (engine: Engine, columns: readonly ResultColumn[], s
|
|
|
58
65
|
referenceAt: (column, cells) => {
|
|
59
66
|
const key = foreignKeyAt(column);
|
|
60
67
|
const source = columns[column]?.source;
|
|
68
|
+
|
|
61
69
|
if (key === undefined || source === undefined) {
|
|
62
70
|
return null;
|
|
63
71
|
}
|
|
72
|
+
|
|
64
73
|
// The columns of the result as the columns of that table, so a join names each key column of its own table only.
|
|
65
74
|
const asTable = columns.map((entry) => ({
|
|
66
75
|
name: entry.source !== undefined && tableKey(entry.source) === tableKey(source) ? entry.source.column : '',
|
|
67
76
|
kind: entry.kind
|
|
68
77
|
}));
|
|
78
|
+
|
|
69
79
|
return referenceOf(engine, key, asTable, cells);
|
|
70
80
|
}
|
|
71
81
|
};
|
|
@@ -82,11 +92,15 @@ export function useResultKeys(session: DatabaseSession | null, engine: Engine |
|
|
|
82
92
|
if (session === null || result?.kind !== 'rows') {
|
|
83
93
|
return undefined;
|
|
84
94
|
}
|
|
95
|
+
|
|
85
96
|
const tables = sourceTablesOf(result.columns);
|
|
97
|
+
|
|
86
98
|
if (tables.length === 0) {
|
|
87
99
|
return undefined;
|
|
88
100
|
}
|
|
101
|
+
|
|
89
102
|
const controller = new AbortController();
|
|
103
|
+
|
|
90
104
|
// A table the server named but cannot describe, such as a derived one, marks no column.
|
|
91
105
|
const loads = tables.map((table) =>
|
|
92
106
|
session.structure(table.schema, table.table, { signal: controller.signal }).then(
|
|
@@ -94,11 +108,13 @@ export function useResultKeys(session: DatabaseSession | null, engine: Engine |
|
|
|
94
108
|
() => null
|
|
95
109
|
)
|
|
96
110
|
);
|
|
111
|
+
|
|
97
112
|
void Promise.all(loads).then((entries) => {
|
|
98
113
|
if (!controller.signal.aborted) {
|
|
99
114
|
setLoaded({ result, structures: new Map(entries.filter((entry) => entry !== null)) });
|
|
100
115
|
}
|
|
101
116
|
});
|
|
117
|
+
|
|
102
118
|
return () => controller.abort();
|
|
103
119
|
}, [session, result]);
|
|
104
120
|
|
package/src/console/shortcuts.ts
CHANGED
|
@@ -10,8 +10,10 @@ export const runScopeOf = (event: KeyLike, apple: boolean): QueryConsoleRunScope
|
|
|
10
10
|
if (matchesShortcut(RUN_ALL_SHORTCUT, event, apple)) {
|
|
11
11
|
return 'all';
|
|
12
12
|
}
|
|
13
|
+
|
|
13
14
|
if (matchesShortcut(RUN_SHORTCUT, event, apple)) {
|
|
14
15
|
return 'selection-or-statement';
|
|
15
16
|
}
|
|
17
|
+
|
|
16
18
|
return null;
|
|
17
19
|
};
|
package/src/ddl/alter-mysql.ts
CHANGED
|
@@ -16,9 +16,11 @@ const withPosition = (clause: string, position: string | null): string => (posit
|
|
|
16
16
|
export const mysqlAlterSql = (dialect: Dialect, schema: string, original: TableDraft, draft: TableDraft, diff: TableDiff): string[] => {
|
|
17
17
|
const early: string[] = [];
|
|
18
18
|
const late: string[] = [];
|
|
19
|
+
|
|
19
20
|
const column = (verb: 'CHANGE' | 'MODIFY' | 'ADD', target: ColumnDraft, previous: string | null, position: string | null): string => {
|
|
20
21
|
const spec = columnSpecSql(dialect, target);
|
|
21
22
|
const named = verb === 'CHANGE' ? `${quote(dialect, previous!)} ${quote(dialect, target.name)}` : quote(dialect, target.name);
|
|
23
|
+
|
|
22
24
|
return withPosition(`${verb} COLUMN ${named} ${spec}`, position);
|
|
23
25
|
};
|
|
24
26
|
|
|
@@ -29,25 +31,31 @@ export const mysqlAlterSql = (dialect: Dialect, schema: string, original: TableD
|
|
|
29
31
|
for (const foreignKey of diff.droppedForeignKeys) {
|
|
30
32
|
early.push(`DROP FOREIGN KEY ${quote(dialect, foreignKey.name)}`);
|
|
31
33
|
}
|
|
34
|
+
|
|
32
35
|
for (const index of diff.droppedIndexes) {
|
|
33
36
|
early.push(`DROP INDEX ${quote(dialect, index.name)}`);
|
|
34
37
|
}
|
|
38
|
+
|
|
35
39
|
for (const { from, to } of diff.renamedIndexes) {
|
|
36
40
|
early.push(`RENAME INDEX ${quote(dialect, from.name)} TO ${quote(dialect, to.name)}`);
|
|
37
41
|
}
|
|
42
|
+
|
|
38
43
|
if (diff.primaryKeyChanged && original.primaryKey.length > 0) {
|
|
39
44
|
early.push('DROP PRIMARY KEY');
|
|
40
45
|
}
|
|
46
|
+
|
|
41
47
|
for (const dropped of diff.droppedColumns) {
|
|
42
48
|
early.push(`DROP COLUMN ${quote(dialect, dropped.name)}`);
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
for (const kept of diff.keptColumns) {
|
|
46
52
|
const verb = kept.renamed ? 'CHANGE' : 'MODIFY';
|
|
53
|
+
|
|
47
54
|
if (split) {
|
|
48
55
|
if (kept.renamed || kept.redefined) {
|
|
49
56
|
early.push(column(verb, kept.column, kept.original.name, null));
|
|
50
57
|
}
|
|
58
|
+
|
|
51
59
|
if (kept.moved) {
|
|
52
60
|
late.push(column('MODIFY', kept.column, null, positionSql(dialect, kept.after)));
|
|
53
61
|
}
|
|
@@ -55,6 +63,7 @@ export const mysqlAlterSql = (dialect: Dialect, schema: string, original: TableD
|
|
|
55
63
|
early.push(column(verb, kept.column, kept.original.name, kept.moved ? positionSql(dialect, kept.after) : null));
|
|
56
64
|
}
|
|
57
65
|
}
|
|
66
|
+
|
|
58
67
|
for (const added of diff.addedColumns) {
|
|
59
68
|
late.push(column('ADD', added.column, null, added.placed ? positionSql(dialect, added.after) : null));
|
|
60
69
|
}
|
|
@@ -62,21 +71,28 @@ export const mysqlAlterSql = (dialect: Dialect, schema: string, original: TableD
|
|
|
62
71
|
if (diff.primaryKeyChanged && draft.primaryKey.length > 0) {
|
|
63
72
|
late.push(`ADD PRIMARY KEY (${columnList(dialect, draft.primaryKey)})`);
|
|
64
73
|
}
|
|
74
|
+
|
|
65
75
|
for (const index of diff.addedIndexes) {
|
|
66
76
|
late.push(`ADD ${inlineIndexSql(dialect, index)}`);
|
|
67
77
|
}
|
|
78
|
+
|
|
68
79
|
for (const foreignKey of diff.addedForeignKeys) {
|
|
69
80
|
late.push(`ADD ${foreignKeySql(dialect, schema, foreignKey)}`);
|
|
70
81
|
}
|
|
82
|
+
|
|
71
83
|
const options = diff.optionsChanged ? mysqlOptionsSql(draft.options, original.options).join(' ') : '';
|
|
84
|
+
|
|
72
85
|
if (options !== '') {
|
|
73
86
|
late.push(options);
|
|
74
87
|
}
|
|
88
|
+
|
|
75
89
|
if (diff.renamed) {
|
|
76
90
|
late.push(`RENAME TO ${quote(dialect, draft.name)}`);
|
|
77
91
|
}
|
|
78
92
|
|
|
79
93
|
const target = tableName(dialect, schema, original.name);
|
|
94
|
+
|
|
80
95
|
const statement = (clauses: readonly string[]): string[] => (clauses.length === 0 ? [] : [`ALTER TABLE ${target}\n ${clauses.join(',\n ')}`]);
|
|
96
|
+
|
|
81
97
|
return split ? [...statement(early), ...statement(late)] : statement([...early, ...late]);
|
|
82
98
|
};
|
package/src/ddl/alter-sqlite.ts
CHANGED
|
@@ -13,12 +13,16 @@ const addable = (column: ColumnDraft): boolean => {
|
|
|
13
13
|
if (column.autoIncrement || (column.generated && column.generatedClause !== null && /\bSTORED\b/i.test(column.generatedClause))) {
|
|
14
14
|
return false;
|
|
15
15
|
}
|
|
16
|
+
|
|
16
17
|
const expression = column.defaultValue?.trim() ?? '';
|
|
18
|
+
|
|
17
19
|
if (expression === '') {
|
|
18
20
|
return column.nullable;
|
|
19
21
|
}
|
|
22
|
+
|
|
20
23
|
// Only a constant may fill existing rows: not an expression, not the clock.
|
|
21
24
|
const clause = defaultClause(expression);
|
|
25
|
+
|
|
22
26
|
return !clause.startsWith('(') && !/^CURRENT_|^LOCALTIME|^NOW/i.test(clause) && (column.nullable || !/^NULL$/i.test(clause));
|
|
23
27
|
};
|
|
24
28
|
|
|
@@ -26,19 +30,25 @@ const needsRebuild = (dialect: Dialect, original: TableDraft, diff: TableDiff):
|
|
|
26
30
|
if (diff.optionsChanged || diff.primaryKeyChanged || diff.droppedForeignKeys.length > 0 || diff.addedForeignKeys.length > 0) {
|
|
27
31
|
return true;
|
|
28
32
|
}
|
|
33
|
+
|
|
29
34
|
if (diff.keptColumns.some((kept) => kept.redefined || kept.moved || (kept.renamed && !atLeast(dialect, ...RENAME_COLUMN)))) {
|
|
30
35
|
return true;
|
|
31
36
|
}
|
|
37
|
+
|
|
32
38
|
if (diff.addedColumns.some((added) => added.placed || !addable(added.column))) {
|
|
33
39
|
return true;
|
|
34
40
|
}
|
|
41
|
+
|
|
35
42
|
if (diff.droppedColumns.length > 0 && !atLeast(dialect, ...DROP_COLUMN)) {
|
|
36
43
|
return true;
|
|
37
44
|
}
|
|
45
|
+
|
|
38
46
|
const autoIndexed = new Set(original.indexes.filter((index) => isAutoIndex(index.name)).flatMap((index) => index.columns));
|
|
47
|
+
|
|
39
48
|
if (diff.droppedColumns.some((dropped) => autoIndexed.has(dropped.name))) {
|
|
40
49
|
return true;
|
|
41
50
|
}
|
|
51
|
+
|
|
42
52
|
return [...diff.droppedIndexes, ...diff.renamedIndexes.map(({ from }) => from)].some((index) => isAutoIndex(index.name));
|
|
43
53
|
};
|
|
44
54
|
|
|
@@ -48,11 +58,13 @@ const rebuildSql = (dialect: Dialect, schema: string, original: TableDraft, draf
|
|
|
48
58
|
const { table, indexes } = createTableParts(dialect, schema, draft, temporary);
|
|
49
59
|
const copied = diff.keptColumns.filter((kept) => !kept.column.generated);
|
|
50
60
|
const statements = ['PRAGMA foreign_keys=OFF', 'BEGIN', table];
|
|
61
|
+
|
|
51
62
|
if (copied.length > 0) {
|
|
52
63
|
const into = copied.map((kept) => quote(dialect, kept.column.name)).join(', ');
|
|
53
64
|
const from = copied.map((kept) => quote(dialect, kept.original.name)).join(', ');
|
|
54
65
|
statements.push(`INSERT INTO ${tableName(dialect, schema, temporary)} (${into}) SELECT ${from} FROM ${tableName(dialect, schema, original.name)}`);
|
|
55
66
|
}
|
|
67
|
+
|
|
56
68
|
statements.push(
|
|
57
69
|
`DROP TABLE ${tableName(dialect, schema, original.name)}`,
|
|
58
70
|
`ALTER TABLE ${tableName(dialect, schema, temporary)} RENAME TO ${quote(dialect, draft.name)}`,
|
|
@@ -61,6 +73,7 @@ const rebuildSql = (dialect: Dialect, schema: string, original: TableDraft, draf
|
|
|
61
73
|
'COMMIT',
|
|
62
74
|
'PRAGMA foreign_keys=ON'
|
|
63
75
|
);
|
|
76
|
+
|
|
64
77
|
return statements;
|
|
65
78
|
};
|
|
66
79
|
|
|
@@ -69,25 +82,33 @@ export const sqliteAlterSql = (dialect: Dialect, schema: string, original: Table
|
|
|
69
82
|
if (needsRebuild(dialect, original, diff)) {
|
|
70
83
|
return rebuildSql(dialect, schema, original, draft, diff);
|
|
71
84
|
}
|
|
85
|
+
|
|
72
86
|
const target = tableName(dialect, schema, original.name);
|
|
73
87
|
const statements: string[] = [];
|
|
88
|
+
|
|
74
89
|
for (const index of [...diff.droppedIndexes, ...diff.renamedIndexes.map(({ from }) => from)]) {
|
|
75
90
|
statements.push(dropIndexSql(dialect, schema, original.name, index.name));
|
|
76
91
|
}
|
|
92
|
+
|
|
77
93
|
for (const dropped of diff.droppedColumns) {
|
|
78
94
|
statements.push(`ALTER TABLE ${target} DROP COLUMN ${quote(dialect, dropped.name)}`);
|
|
79
95
|
}
|
|
96
|
+
|
|
80
97
|
for (const kept of diff.keptColumns.filter((candidate) => candidate.renamed)) {
|
|
81
98
|
statements.push(`ALTER TABLE ${target} RENAME COLUMN ${quote(dialect, kept.original.name)} TO ${quote(dialect, kept.column.name)}`);
|
|
82
99
|
}
|
|
100
|
+
|
|
83
101
|
for (const added of diff.addedColumns) {
|
|
84
102
|
statements.push(`ALTER TABLE ${target} ADD COLUMN ${columnSql(dialect, added.column)}`);
|
|
85
103
|
}
|
|
104
|
+
|
|
86
105
|
for (const index of [...diff.addedIndexes, ...diff.renamedIndexes.map(({ to }) => to)]) {
|
|
87
106
|
statements.push(createIndexSql(dialect, schema, original.name, index));
|
|
88
107
|
}
|
|
108
|
+
|
|
89
109
|
if (diff.renamed) {
|
|
90
110
|
statements.push(`ALTER TABLE ${target} RENAME TO ${quote(dialect, draft.name)}`);
|
|
91
111
|
}
|
|
112
|
+
|
|
92
113
|
return statements;
|
|
93
114
|
};
|
package/src/ddl/alter.ts
CHANGED
|
@@ -22,8 +22,10 @@ const unchanged = (diff: TableDiff): boolean =>
|
|
|
22
22
|
export const alterTableSql = (dialect: Dialect, schema: string, structure: TableStructure, draft: TableDraft): string[] => {
|
|
23
23
|
const original = draftOf(structure);
|
|
24
24
|
const diff = diffOf(dialect, original, draft);
|
|
25
|
+
|
|
25
26
|
if (unchanged(diff)) {
|
|
26
27
|
return [];
|
|
27
28
|
}
|
|
29
|
+
|
|
28
30
|
return dialect.engine === 'sqlite' ? sqliteAlterSql(dialect, schema, original, draft, diff) : mysqlAlterSql(dialect, schema, original, draft, diff);
|
|
29
31
|
};
|
package/src/ddl/column.ts
CHANGED
|
@@ -11,9 +11,11 @@ const KEYWORD = /^(?:NULL|TRUE|FALSE|CURRENT_DATE|CURRENT_TIME(?:\(\d*\))?|CURRE
|
|
|
11
11
|
*/
|
|
12
12
|
export const defaultClause = (expression: string): string => {
|
|
13
13
|
const text = expression.trim();
|
|
14
|
+
|
|
14
15
|
if (NUMERIC_TEXT.test(text) || STRING.test(text) || KEYWORD.test(text)) {
|
|
15
16
|
return text;
|
|
16
17
|
}
|
|
18
|
+
|
|
17
19
|
return text.startsWith('(') && text.endsWith(')') ? text : `(${text})`;
|
|
18
20
|
};
|
|
19
21
|
|
|
@@ -26,34 +28,43 @@ export interface ColumnSqlOptions {
|
|
|
26
28
|
export const columnSpecSql = (dialect: Dialect, column: ColumnDraft, { inlinePrimaryKey = false }: ColumnSqlOptions = {}): string => {
|
|
27
29
|
const mysql = dialect.engine === 'mysql';
|
|
28
30
|
const parts: string[] = [];
|
|
31
|
+
|
|
29
32
|
if (column.type !== '') {
|
|
30
33
|
parts.push(column.type);
|
|
31
34
|
}
|
|
35
|
+
|
|
32
36
|
if (column.generated && column.generatedClause !== null) {
|
|
33
37
|
parts.push(column.generatedClause);
|
|
34
38
|
}
|
|
39
|
+
|
|
35
40
|
if (inlinePrimaryKey) {
|
|
36
41
|
parts.push(column.autoIncrement ? 'PRIMARY KEY AUTOINCREMENT' : 'PRIMARY KEY');
|
|
37
42
|
}
|
|
43
|
+
|
|
38
44
|
// MariaDB takes neither NULL nor NOT NULL after the expression of a computed column.
|
|
39
45
|
const statesNullability = !(column.generated && dialect.flavor === 'mariadb');
|
|
46
|
+
|
|
40
47
|
if (statesNullability && !column.nullable) {
|
|
41
48
|
parts.push('NOT NULL');
|
|
42
49
|
} else if (statesNullability && mysql) {
|
|
43
50
|
// MySQL gives a TIMESTAMP column NOT NULL unless it is told otherwise.
|
|
44
51
|
parts.push('NULL');
|
|
45
52
|
}
|
|
53
|
+
|
|
46
54
|
if (!column.generated) {
|
|
47
55
|
if (column.defaultValue !== null && column.defaultValue.trim() !== '') {
|
|
48
56
|
parts.push(`DEFAULT ${defaultClause(column.defaultValue)}`);
|
|
49
57
|
}
|
|
58
|
+
|
|
50
59
|
if (mysql && column.autoIncrement) {
|
|
51
60
|
parts.push('AUTO_INCREMENT');
|
|
52
61
|
}
|
|
53
62
|
}
|
|
63
|
+
|
|
54
64
|
if (mysql && column.comment !== '') {
|
|
55
65
|
parts.push(`COMMENT ${sqlLiteral('mysql', column.comment)}`);
|
|
56
66
|
}
|
|
67
|
+
|
|
57
68
|
return parts.join(' ');
|
|
58
69
|
};
|
|
59
70
|
|
package/src/ddl/create.ts
CHANGED
|
@@ -9,22 +9,27 @@ const INDENT = ' ';
|
|
|
9
9
|
/* `CONSTRAINT name FOREIGN KEY (...) REFERENCES t (...) ON UPDATE .. ON DELETE ..`, shared by a create and an `ADD`. */
|
|
10
10
|
export const foreignKeySql = (dialect: Dialect, schema: string, foreignKey: ForeignKeyDraft): string => {
|
|
11
11
|
const sqlite = dialect.engine === 'sqlite';
|
|
12
|
+
|
|
12
13
|
// SQLite cannot name another schema in a reference, and MySQL needs one only for another database.
|
|
13
14
|
const target =
|
|
14
15
|
sqlite || foreignKey.referencedSchema === '' || foreignKey.referencedSchema === schema
|
|
15
16
|
? quote(dialect, foreignKey.referencedTable)
|
|
16
17
|
: `${quote(dialect, foreignKey.referencedSchema)}.${quote(dialect, foreignKey.referencedTable)}`;
|
|
18
|
+
|
|
17
19
|
const parts = [
|
|
18
20
|
...(foreignKey.name === '' ? [] : [`CONSTRAINT ${quote(dialect, foreignKey.name)}`]),
|
|
19
21
|
`FOREIGN KEY (${columnList(dialect, foreignKey.columns)})`,
|
|
20
22
|
`REFERENCES ${target} (${columnList(dialect, foreignKey.referencedColumns)})`
|
|
21
23
|
];
|
|
24
|
+
|
|
22
25
|
if (foreignKey.onUpdate !== null) {
|
|
23
26
|
parts.push(`ON UPDATE ${foreignKey.onUpdate}`);
|
|
24
27
|
}
|
|
28
|
+
|
|
25
29
|
if (foreignKey.onDelete !== null) {
|
|
26
30
|
parts.push(`ON DELETE ${foreignKey.onDelete}`);
|
|
27
31
|
}
|
|
32
|
+
|
|
28
33
|
return parts.join(' ');
|
|
29
34
|
};
|
|
30
35
|
|
|
@@ -33,6 +38,7 @@ export const mysqlOptionsSql = (options: TableOptions, against?: TableOptions):
|
|
|
33
38
|
// Only a comment can be emptied; an engine, a character set or a collation has no empty value to ask for.
|
|
34
39
|
const changed = (field: 'engine' | 'charset' | 'collation' | 'comment'): boolean =>
|
|
35
40
|
options[field] !== against?.[field] && (options[field] !== '' || (field === 'comment' && against !== undefined));
|
|
41
|
+
|
|
36
42
|
return [
|
|
37
43
|
...(changed('engine') ? [`ENGINE=${options.engine}`] : []),
|
|
38
44
|
...(changed('charset') ? [`DEFAULT CHARSET=${options.charset}`] : []),
|
|
@@ -59,16 +65,21 @@ export interface CreateParts {
|
|
|
59
65
|
/* The `CREATE TABLE` of a draft under a name of the caller's choosing, which a rebuild uses for the table it fills before the swap. */
|
|
60
66
|
export const createTableParts = (dialect: Dialect, schema: string, draft: TableDraft, name: string): CreateParts => {
|
|
61
67
|
const sqlite = dialect.engine === 'sqlite';
|
|
68
|
+
|
|
62
69
|
// SQLite declares AUTOINCREMENT on the column itself, which then is the whole primary key; a WITHOUT ROWID table has no rowid to count.
|
|
63
70
|
const autoColumn =
|
|
64
71
|
sqlite && draft.primaryKey.length === 1 && !draft.options.withoutRowid
|
|
65
72
|
? draft.columns.find((column) => column.name === draft.primaryKey[0] && column.autoIncrement)?.name
|
|
66
73
|
: undefined;
|
|
74
|
+
|
|
67
75
|
const lines = draft.columns.map((column) => columnSql(dialect, column, { inlinePrimaryKey: column.name === autoColumn }));
|
|
76
|
+
|
|
68
77
|
if (draft.primaryKey.length > 0 && autoColumn === undefined) {
|
|
69
78
|
lines.push(`PRIMARY KEY (${columnList(dialect, draft.primaryKey)})`);
|
|
70
79
|
}
|
|
80
|
+
|
|
71
81
|
const separate: IndexDraft[] = [];
|
|
82
|
+
|
|
72
83
|
for (const index of draft.indexes) {
|
|
73
84
|
if (!sqlite) {
|
|
74
85
|
lines.push(inlineIndexSql(dialect, index));
|
|
@@ -78,15 +89,19 @@ export const createTableParts = (dialect: Dialect, schema: string, draft: TableD
|
|
|
78
89
|
separate.push(index);
|
|
79
90
|
}
|
|
80
91
|
}
|
|
92
|
+
|
|
81
93
|
for (const foreignKey of draft.foreignKeys) {
|
|
82
94
|
lines.push(foreignKeySql(dialect, schema, foreignKey));
|
|
83
95
|
}
|
|
96
|
+
|
|
84
97
|
const options = sqlite ? sqliteOptionsSql(draft.options).join(', ') : mysqlOptionsSql(draft.options).join(' ');
|
|
85
98
|
const head = `CREATE TABLE ${tableName(dialect, schema, name)} (\n${lines.map((line) => INDENT + line).join(',\n')}\n)`;
|
|
99
|
+
|
|
86
100
|
return { table: options === '' ? head : `${head} ${options}`, indexes: separate };
|
|
87
101
|
};
|
|
88
102
|
|
|
89
103
|
export const createTableSql = (dialect: Dialect, schema: string, draft: TableDraft): string[] => {
|
|
90
104
|
const { table, indexes } = createTableParts(dialect, schema, draft, draft.name);
|
|
105
|
+
|
|
91
106
|
return [table, ...indexes.map((index) => createIndexSql(dialect, schema, draft.name, index))];
|
|
92
107
|
};
|
package/src/ddl/dialect.ts
CHANGED
|
@@ -25,12 +25,15 @@ export const atLeast = (dialect: Dialect, ...wanted: number[]): boolean => {
|
|
|
25
25
|
if (dialect.version.length === 0) {
|
|
26
26
|
return true;
|
|
27
27
|
}
|
|
28
|
+
|
|
28
29
|
for (let i = 0; i < wanted.length; i++) {
|
|
29
30
|
const have = dialect.version[i] ?? 0;
|
|
31
|
+
|
|
30
32
|
if (have !== wanted[i]) {
|
|
31
33
|
return have > wanted[i]!;
|
|
32
34
|
}
|
|
33
35
|
}
|
|
36
|
+
|
|
34
37
|
return true;
|
|
35
38
|
};
|
|
36
39
|
|
|
@@ -47,6 +50,7 @@ export const tableName = (dialect: Dialect, schema: string, table: string): stri
|
|
|
47
50
|
/* SQLite puts the schema in front of the name of an index, where MySQL names the table it is on. */
|
|
48
51
|
export const sqliteSchemaPrefix = (dialect: Dialect, schema: string): string => {
|
|
49
52
|
const named = schemaOf(dialect, schema);
|
|
53
|
+
|
|
50
54
|
return named === undefined ? '' : `${quote(dialect, named)}.`;
|
|
51
55
|
};
|
|
52
56
|
|
package/src/ddl/diff.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { sameDefinition } from './column.ts';
|
|
|
2
2
|
import type { Dialect } from './dialect.ts';
|
|
3
3
|
import type { ColumnDraft, ForeignKeyDraft, IndexDraft, TableDraft } from './draft.ts';
|
|
4
4
|
import type { TableOptions } from './parse.ts';
|
|
5
|
+
import { sameList } from '../same-list.ts';
|
|
5
6
|
|
|
6
7
|
export interface KeptColumn {
|
|
7
8
|
readonly original: ColumnDraft;
|
|
@@ -41,30 +42,34 @@ export interface TableDiff {
|
|
|
41
42
|
readonly optionsChanged: boolean;
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
const sameList = (left: readonly string[], right: readonly string[]): boolean => left.length === right.length && left.every((name, at) => name === right[at]);
|
|
45
|
-
|
|
46
45
|
/* The positions of a longest run that already is in order: those columns can stay where they are. */
|
|
47
46
|
const longestRun = (positions: readonly number[]): ReadonlySet<number> => {
|
|
48
47
|
const length: number[] = [];
|
|
49
48
|
const previous: number[] = [];
|
|
50
49
|
let best = -1;
|
|
50
|
+
|
|
51
51
|
for (let i = 0; i < positions.length; i++) {
|
|
52
52
|
length[i] = 1;
|
|
53
53
|
previous[i] = -1;
|
|
54
|
+
|
|
54
55
|
for (let before = 0; before < i; before++) {
|
|
55
56
|
if (positions[before]! < positions[i]! && length[before]! + 1 > length[i]!) {
|
|
56
57
|
length[i] = length[before]! + 1;
|
|
57
58
|
previous[i] = before;
|
|
58
59
|
}
|
|
59
60
|
}
|
|
61
|
+
|
|
60
62
|
if (best < 0 || length[i]! > length[best]!) {
|
|
61
63
|
best = i;
|
|
62
64
|
}
|
|
63
65
|
}
|
|
66
|
+
|
|
64
67
|
const run = new Set<number>();
|
|
68
|
+
|
|
65
69
|
for (let at = best; at >= 0; at = previous[at]!) {
|
|
66
70
|
run.add(at);
|
|
67
71
|
}
|
|
72
|
+
|
|
68
73
|
return run;
|
|
69
74
|
};
|
|
70
75
|
|
|
@@ -82,17 +87,23 @@ const columnChanges = (dialect: Dialect, original: TableDraft, draft: TableDraft
|
|
|
82
87
|
let survivor = 0;
|
|
83
88
|
const keptColumns: KeptColumn[] = [];
|
|
84
89
|
const addedColumns: AddedColumn[] = [];
|
|
90
|
+
|
|
85
91
|
draft.columns.forEach((column, at) => {
|
|
86
92
|
const after = at === 0 ? null : draft.columns[at - 1]!.name;
|
|
93
|
+
|
|
87
94
|
if (column.originalName === null) {
|
|
88
95
|
addedColumns.push({ column, placed: draft.columns.slice(at + 1).some((later) => later.originalName !== null), after });
|
|
96
|
+
|
|
89
97
|
return;
|
|
90
98
|
}
|
|
99
|
+
|
|
91
100
|
const rank = survivor++;
|
|
92
101
|
const before = original.columns.find((candidate) => candidate.name === column.originalName);
|
|
102
|
+
|
|
93
103
|
if (before === undefined) {
|
|
94
104
|
return;
|
|
95
105
|
}
|
|
106
|
+
|
|
96
107
|
keptColumns.push({
|
|
97
108
|
original: before,
|
|
98
109
|
column,
|
|
@@ -104,6 +115,7 @@ const columnChanges = (dialect: Dialect, original: TableDraft, draft: TableDraft
|
|
|
104
115
|
});
|
|
105
116
|
|
|
106
117
|
const droppedColumns = original.columns.filter((column) => !survivors.some((kept) => kept.originalName === column.name));
|
|
118
|
+
|
|
107
119
|
return { droppedColumns, keptColumns, addedColumns };
|
|
108
120
|
};
|
|
109
121
|
|
|
@@ -111,8 +123,10 @@ const indexChanges = (original: TableDraft, draft: TableDraft, project: Project)
|
|
|
111
123
|
const droppedIndexes: IndexDraft[] = [];
|
|
112
124
|
const addedIndexes = draft.indexes.filter((index) => index.originalName === null);
|
|
113
125
|
const renamedIndexes: RenamedIndex[] = [];
|
|
126
|
+
|
|
114
127
|
for (const before of original.indexes) {
|
|
115
128
|
const after = draft.indexes.find((index) => matches(before, index));
|
|
129
|
+
|
|
116
130
|
if (after === undefined) {
|
|
117
131
|
droppedIndexes.push(before);
|
|
118
132
|
} else if (!sameList(project(before.columns), after.columns) || before.unique !== after.unique) {
|
|
@@ -122,6 +136,7 @@ const indexChanges = (original: TableDraft, draft: TableDraft, project: Project)
|
|
|
122
136
|
renamedIndexes.push({ from: before, to: after });
|
|
123
137
|
}
|
|
124
138
|
}
|
|
139
|
+
|
|
125
140
|
return { droppedIndexes, addedIndexes, renamedIndexes };
|
|
126
141
|
};
|
|
127
142
|
|
|
@@ -137,21 +152,26 @@ const sameForeignKey = (before: ForeignKeyDraft, after: ForeignKeyDraft, project
|
|
|
137
152
|
const foreignKeyChanges = (original: TableDraft, draft: TableDraft, project: Project) => {
|
|
138
153
|
const droppedForeignKeys: ForeignKeyDraft[] = [];
|
|
139
154
|
const addedForeignKeys = draft.foreignKeys.filter((key) => key.originalName === null);
|
|
155
|
+
|
|
140
156
|
for (const before of original.foreignKeys) {
|
|
141
157
|
const after = draft.foreignKeys.find((key) => matches(before, key));
|
|
158
|
+
|
|
142
159
|
if (after === undefined || !sameForeignKey(before, after, project)) {
|
|
143
160
|
droppedForeignKeys.push(before);
|
|
161
|
+
|
|
144
162
|
if (after !== undefined) {
|
|
145
163
|
addedForeignKeys.push(after);
|
|
146
164
|
}
|
|
147
165
|
}
|
|
148
166
|
}
|
|
167
|
+
|
|
149
168
|
return { droppedForeignKeys, addedForeignKeys };
|
|
150
169
|
};
|
|
151
170
|
|
|
152
171
|
/* What a draft changes in the table it came from, told apart by the names items kept. */
|
|
153
172
|
export const diffOf = (dialect: Dialect, original: TableDraft, draft: TableDraft): TableDiff => {
|
|
154
173
|
const renames = new Map(draft.columns.filter((column) => column.originalName !== null).map((column) => [column.originalName!, column.name]));
|
|
174
|
+
|
|
155
175
|
const project: Project = (names) => names.map((name) => renames.get(name) ?? name);
|
|
156
176
|
|
|
157
177
|
return {
|
package/src/ddl/draft.ts
CHANGED
|
@@ -105,6 +105,7 @@ const foreignKeyDraftOf = (foreignKey: ForeignKeyInfo, at: number): ForeignKeyDr
|
|
|
105
105
|
/* The draft that changes nothing: what a structure already says. */
|
|
106
106
|
export const draftOf = (structure: TableStructure): TableDraft => {
|
|
107
107
|
const clauses = generatedClausesOf(structure.ddl);
|
|
108
|
+
|
|
108
109
|
return {
|
|
109
110
|
originalName: structure.name,
|
|
110
111
|
name: structure.name,
|