@adecore/database 0.0.1 → 0.16.0-beta.0
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 +21 -0
- package/README.md +152 -1
- package/dist/DatabaseProvider.d.ts +13 -0
- package/dist/DatabaseProvider.js +17 -0
- package/dist/actions.d.ts +40 -0
- package/dist/actions.js +1 -0
- package/dist/client/client.d.ts +5 -0
- package/dist/client/client.js +245 -0
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +2 -0
- package/dist/client/types.d.ts +97 -0
- package/dist/client/types.js +13 -0
- package/dist/client-context.d.ts +16 -0
- package/dist/client-context.js +17 -0
- package/dist/connections/ConnectionForm.d.ts +10 -0
- package/dist/connections/ConnectionForm.js +145 -0
- package/dist/connections/ConnectionManager.d.ts +12 -0
- package/dist/connections/ConnectionManager.js +93 -0
- package/dist/connections/EngineIcon.d.ts +9 -0
- package/dist/connections/EngineIcon.js +7 -0
- package/dist/connections/connection-config.d.ts +16 -0
- package/dist/connections/connection-config.js +107 -0
- package/dist/connections/docker.d.ts +9 -0
- package/dist/connections/docker.js +38 -0
- package/dist/connections/engine-icons.d.ts +3 -0
- package/dist/connections/engine-icons.js +3 -0
- package/dist/connections/use-docker-containers.d.ts +17 -0
- package/dist/connections/use-docker-containers.js +26 -0
- package/dist/console/DestructiveDialog.d.ts +9 -0
- package/dist/console/DestructiveDialog.js +9 -0
- package/dist/console/HistoryPanel.d.ts +10 -0
- package/dist/console/HistoryPanel.js +15 -0
- package/dist/console/QueryConsole.d.ts +14 -0
- package/dist/console/QueryConsole.js +261 -0
- package/dist/console/SchemaPicker.d.ts +11 -0
- package/dist/console/SchemaPicker.js +8 -0
- package/dist/console/SqlEditor.d.ts +12 -0
- package/dist/console/SqlEditor.js +61 -0
- package/dist/console/StatementResultView.d.ts +18 -0
- package/dist/console/StatementResultView.js +32 -0
- package/dist/console/TransactionControls.d.ts +13 -0
- package/dist/console/TransactionControls.js +12 -0
- package/dist/console/destructive.d.ts +7 -0
- package/dist/console/destructive.js +43 -0
- package/dist/console/editing.d.ts +12 -0
- package/dist/console/editing.js +60 -0
- package/dist/console/history.d.ts +13 -0
- package/dist/console/history.js +43 -0
- package/dist/console/labels.d.ts +1 -0
- package/dist/console/labels.js +6 -0
- package/dist/console/outcome.d.ts +6 -0
- package/dist/console/outcome.js +11 -0
- package/dist/console/schemas.d.ts +2 -0
- package/dist/console/schemas.js +5 -0
- package/dist/console/shortcuts.d.ts +2 -0
- package/dist/console/shortcuts.js +3 -0
- package/dist/console/useConsoleHistory.d.ts +7 -0
- package/dist/console/useConsoleHistory.js +22 -0
- package/dist/ddl/alter-mysql.d.ts +4 -0
- package/dist/ddl/alter-mysql.js +79 -0
- package/dist/ddl/alter-sqlite.d.ts +4 -0
- package/dist/ddl/alter-sqlite.js +80 -0
- package/dist/ddl/alter.d.ts +4 -0
- package/dist/ddl/alter.js +24 -0
- package/dist/ddl/column.d.ts +9 -0
- package/dist/ddl/column.js +62 -0
- package/dist/ddl/create.d.ts +12 -0
- package/dist/ddl/create.js +74 -0
- package/dist/ddl/dialect.d.ts +12 -0
- package/dist/ddl/dialect.js +34 -0
- package/dist/ddl/diff.d.ts +34 -0
- package/dist/ddl/diff.js +111 -0
- package/dist/ddl/draft.d.ts +46 -0
- package/dist/ddl/draft.js +70 -0
- package/dist/ddl/index.d.ts +10 -0
- package/dist/ddl/index.js +10 -0
- package/dist/ddl/parse.d.ts +17 -0
- package/dist/ddl/parse.js +131 -0
- package/dist/ddl/statements.d.ts +6 -0
- package/dist/ddl/statements.js +12 -0
- package/dist/ddl/types.d.ts +6 -0
- package/dist/ddl/types.js +52 -0
- package/dist/ddl/validate.d.ts +7 -0
- package/dist/ddl/validate.js +38 -0
- package/dist/designer/ColumnsEditor.d.ts +2 -0
- package/dist/designer/ColumnsEditor.js +24 -0
- package/dist/designer/DesignerView.d.ts +25 -0
- package/dist/designer/DesignerView.js +33 -0
- package/dist/designer/ForeignKeysEditor.d.ts +7 -0
- package/dist/designer/ForeignKeysEditor.js +24 -0
- package/dist/designer/IndexesEditor.d.ts +2 -0
- package/dist/designer/IndexesEditor.js +13 -0
- package/dist/designer/NamePicker.d.ts +11 -0
- package/dist/designer/NamePicker.js +13 -0
- package/dist/designer/OptionsEditor.d.ts +2 -0
- package/dist/designer/OptionsEditor.js +17 -0
- package/dist/designer/SqlPreview.d.ts +12 -0
- package/dist/designer/SqlPreview.js +17 -0
- package/dist/designer/TableDesigner.d.ts +10 -0
- package/dist/designer/TableDesigner.js +103 -0
- package/dist/designer/edit.d.ts +17 -0
- package/dist/designer/edit.js +115 -0
- package/dist/designer/editor-props.d.ts +10 -0
- package/dist/designer/editor-props.js +1 -0
- package/dist/designer/plan.d.ts +9 -0
- package/dist/designer/plan.js +13 -0
- package/dist/designer/recover.d.ts +2 -0
- package/dist/designer/recover.js +18 -0
- package/dist/designer/useReferenceColumns.d.ts +3 -0
- package/dist/designer/useReferenceColumns.js +36 -0
- package/dist/explorer/DatabaseExplorer.d.ts +13 -0
- package/dist/explorer/DatabaseExplorer.js +264 -0
- package/dist/explorer/RowMenu.d.ts +11 -0
- package/dist/explorer/RowMenu.js +89 -0
- package/dist/explorer/TableDialog.d.ts +15 -0
- package/dist/explorer/TableDialog.js +50 -0
- package/dist/explorer/expansion.d.ts +7 -0
- package/dist/explorer/expansion.js +31 -0
- package/dist/explorer/table-statements.d.ts +5 -0
- package/dist/explorer/table-statements.js +12 -0
- package/dist/explorer/tree.d.ts +125 -0
- package/dist/explorer/tree.js +295 -0
- package/dist/explorer/use-explorer-loads.d.ts +12 -0
- package/dist/explorer/use-explorer-loads.js +109 -0
- package/dist/explorer/use-stored-expansion.d.ts +9 -0
- package/dist/explorer/use-stored-expansion.js +47 -0
- package/dist/grid/CellEditor.d.ts +9 -0
- package/dist/grid/CellEditor.js +29 -0
- package/dist/grid/CopyAsMenu.d.ts +7 -0
- package/dist/grid/CopyAsMenu.js +10 -0
- package/dist/grid/DataGrid.d.ts +36 -0
- package/dist/grid/DataGrid.js +512 -0
- package/dist/grid/EnumPicker.d.ts +17 -0
- package/dist/grid/EnumPicker.js +53 -0
- package/dist/grid/GridHeaderCell.d.ts +36 -0
- package/dist/grid/GridHeaderCell.js +69 -0
- package/dist/grid/RecordView.d.ts +20 -0
- package/dist/grid/RecordView.js +126 -0
- package/dist/grid/ValueDock.d.ts +11 -0
- package/dist/grid/ValueDock.js +28 -0
- package/dist/grid/aggregates.d.ts +22 -0
- package/dist/grid/aggregates.js +62 -0
- package/dist/grid/cell-range.d.ts +14 -0
- package/dist/grid/cell-range.js +11 -0
- package/dist/grid/column-layout.d.ts +18 -0
- package/dist/grid/column-layout.js +49 -0
- package/dist/grid/column-selection.d.ts +11 -0
- package/dist/grid/column-selection.js +28 -0
- package/dist/grid/column-view.d.ts +10 -0
- package/dist/grid/column-view.js +23 -0
- package/dist/grid/copy-formats.d.ts +15 -0
- package/dist/grid/copy-formats.js +80 -0
- package/dist/grid/display.d.ts +20 -0
- package/dist/grid/display.js +81 -0
- package/dist/grid/edit-value.d.ts +5 -0
- package/dist/grid/edit-value.js +55 -0
- package/dist/grid/enum-type.d.ts +7 -0
- package/dist/grid/enum-type.js +52 -0
- package/dist/grid/focused-value.d.ts +4 -0
- package/dist/grid/focused-value.js +19 -0
- package/dist/grid/layout.d.ts +22 -0
- package/dist/grid/layout.js +74 -0
- package/dist/grid/navigation.d.ts +15 -0
- package/dist/grid/navigation.js +52 -0
- package/dist/grid/row-selection.d.ts +9 -0
- package/dist/grid/row-selection.js +24 -0
- package/dist/grid/sort.d.ts +7 -0
- package/dist/grid/sort.js +7 -0
- package/dist/grid/types.d.ts +30 -0
- package/dist/grid/types.js +9 -0
- package/dist/host/helper-path.d.ts +7 -0
- package/dist/host/helper-path.js +15 -0
- package/dist/host/host.d.ts +2 -0
- package/dist/host/host.js +343 -0
- package/dist/host/index.d.ts +5 -0
- package/dist/host/index.js +4 -0
- package/dist/host/spawn.d.ts +7 -0
- package/dist/host/spawn.js +98 -0
- package/dist/host/types.d.ts +19 -0
- package/dist/host/types.js +1 -0
- package/dist/host/validate.d.ts +10 -0
- package/dist/host/validate.js +339 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +13 -0
- package/dist/locales/en.json +577 -0
- package/dist/locales/nl.json +577 -0
- package/dist/locales.d.ts +4 -0
- package/dist/locales.js +14 -0
- package/dist/protocol/connection.d.ts +56 -0
- package/dist/protocol/connection.js +1 -0
- package/dist/protocol/errors.d.ts +7 -0
- package/dist/protocol/errors.js +1 -0
- package/dist/protocol/index.d.ts +5 -0
- package/dist/protocol/index.js +3 -0
- package/dist/protocol/messages.d.ts +250 -0
- package/dist/protocol/messages.js +2 -0
- package/dist/protocol/schema.d.ts +53 -0
- package/dist/protocol/schema.js +1 -0
- package/dist/protocol/values.d.ts +21 -0
- package/dist/protocol/values.js +15 -0
- package/dist/sql-split.d.ts +15 -0
- package/dist/sql-split.js +201 -0
- package/dist/sql.d.ts +29 -0
- package/dist/sql.js +104 -0
- package/dist/structure/StructureView.d.ts +14 -0
- package/dist/structure/StructureView.js +99 -0
- package/dist/structure/structure-text.d.ts +2 -0
- package/dist/structure/structure-text.js +2 -0
- package/dist/table/CommandField.d.ts +14 -0
- package/dist/table/CommandField.js +157 -0
- package/dist/table/ImportDialog.d.ts +27 -0
- package/dist/table/ImportDialog.js +28 -0
- package/dist/table/TableStatusBar.d.ts +19 -0
- package/dist/table/TableStatusBar.js +22 -0
- package/dist/table/TableToolbar.d.ts +43 -0
- package/dist/table/TableToolbar.js +16 -0
- package/dist/table/TableView.d.ts +13 -0
- package/dist/table/TableView.js +508 -0
- package/dist/table/command-field.d.ts +57 -0
- package/dist/table/command-field.js +203 -0
- package/dist/table/grid-rows.d.ts +23 -0
- package/dist/table/grid-rows.js +55 -0
- package/dist/table/import-mapping.d.ts +7 -0
- package/dist/table/import-mapping.js +28 -0
- package/dist/table/layout-store.d.ts +13 -0
- package/dist/table/layout-store.js +43 -0
- package/dist/table/paging.d.ts +12 -0
- package/dist/table/paging.js +20 -0
- package/dist/table/pending.d.ts +32 -0
- package/dist/table/pending.js +94 -0
- package/dist/table/references.d.ts +12 -0
- package/dist/table/references.js +21 -0
- package/dist/table/useLoaded.d.ts +7 -0
- package/dist/table/useLoaded.js +36 -0
- package/dist/table/useStoredLayout.d.ts +8 -0
- package/dist/table/useStoredLayout.js +39 -0
- package/dist/table/useTableTransfer.d.ts +51 -0
- package/dist/table/useTableTransfer.js +125 -0
- package/dist/testing/fake.d.ts +21 -0
- package/dist/testing/fake.js +372 -0
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +1 -0
- package/dist/use-popup-press.d.ts +1 -0
- package/dist/use-popup-press.js +24 -0
- package/dist/use-schema-change.d.ts +1 -0
- package/dist/use-schema-change.js +13 -0
- package/dist/use-stable-callback.d.ts +1 -0
- package/dist/use-stable-callback.js +9 -0
- package/dist/value/ValuePanel.d.ts +14 -0
- package/dist/value/ValuePanel.js +103 -0
- package/dist/value/draft.d.ts +33 -0
- package/dist/value/draft.js +44 -0
- package/dist/value/hex.d.ts +14 -0
- package/dist/value/hex.js +46 -0
- package/dist/value/json.d.ts +9 -0
- package/dist/value/json.js +71 -0
- package/dist/value/views.d.ts +22 -0
- package/dist/value/views.js +54 -0
- package/dist/workbench/DatabaseWorkbench.d.ts +9 -0
- package/dist/workbench/DatabaseWorkbench.js +179 -0
- package/dist/workbench/DesignerTab.d.ts +10 -0
- package/dist/workbench/DesignerTab.js +22 -0
- package/dist/workbench/TableTab.d.ts +10 -0
- package/dist/workbench/TableTab.js +8 -0
- package/dist/workbench/tabs.d.ts +42 -0
- package/dist/workbench/tabs.js +139 -0
- package/package.json +79 -3
- package/src/DatabaseProvider.tsx +37 -0
- package/src/actions.ts +34 -0
- package/src/client/client.ts +324 -0
- package/src/client/index.ts +18 -0
- package/src/client/types.ts +161 -0
- package/src/client-context.ts +35 -0
- package/src/connections/ConnectionForm.tsx +474 -0
- package/src/connections/ConnectionManager.tsx +224 -0
- package/src/connections/EngineIcon.tsx +18 -0
- package/src/connections/connection-config.ts +143 -0
- package/src/connections/docker.ts +50 -0
- package/src/connections/engine-icons.ts +5 -0
- package/src/connections/use-docker-containers.ts +41 -0
- package/src/console/DestructiveDialog.tsx +40 -0
- package/src/console/HistoryPanel.tsx +67 -0
- package/src/console/QueryConsole.tsx +439 -0
- package/src/console/SchemaPicker.tsx +32 -0
- package/src/console/SqlEditor.tsx +95 -0
- package/src/console/StatementResultView.tsx +176 -0
- package/src/console/TransactionControls.tsx +53 -0
- package/src/console/destructive.ts +60 -0
- package/src/console/editing.ts +77 -0
- package/src/console/history.ts +62 -0
- package/src/console/labels.ts +7 -0
- package/src/console/outcome.ts +14 -0
- package/src/console/schemas.ts +7 -0
- package/src/console/shortcuts.ts +4 -0
- package/src/console/useConsoleHistory.ts +39 -0
- package/src/ddl/alter-mysql.ts +90 -0
- package/src/ddl/alter-sqlite.ts +93 -0
- package/src/ddl/alter.ts +29 -0
- package/src/ddl/column.ts +74 -0
- package/src/ddl/create.ts +92 -0
- package/src/ddl/dialect.ts +53 -0
- package/src/ddl/diff.ts +164 -0
- package/src/ddl/draft.ts +136 -0
- package/src/ddl/index.ts +10 -0
- package/src/ddl/parse.ts +161 -0
- package/src/ddl/statements.ts +20 -0
- package/src/ddl/types.ts +60 -0
- package/src/ddl/validate.ts +47 -0
- package/src/designer/ColumnsEditor.tsx +204 -0
- package/src/designer/DesignerView.tsx +184 -0
- package/src/designer/ForeignKeysEditor.tsx +132 -0
- package/src/designer/IndexesEditor.tsx +63 -0
- package/src/designer/NamePicker.tsx +44 -0
- package/src/designer/OptionsEditor.tsx +101 -0
- package/src/designer/SqlPreview.tsx +56 -0
- package/src/designer/TableDesigner.tsx +186 -0
- package/src/designer/edit.ts +137 -0
- package/src/designer/editor-props.ts +13 -0
- package/src/designer/plan.ts +25 -0
- package/src/designer/recover.ts +20 -0
- package/src/designer/useReferenceColumns.ts +54 -0
- package/src/explorer/DatabaseExplorer.tsx +450 -0
- package/src/explorer/RowMenu.tsx +134 -0
- package/src/explorer/TableDialog.tsx +80 -0
- package/src/explorer/expansion.ts +47 -0
- package/src/explorer/table-statements.ts +18 -0
- package/src/explorer/tree.ts +453 -0
- package/src/explorer/use-explorer-loads.ts +157 -0
- package/src/explorer/use-stored-expansion.ts +66 -0
- package/src/grid/CellEditor.tsx +53 -0
- package/src/grid/CopyAsMenu.tsx +33 -0
- package/src/grid/DataGrid.tsx +871 -0
- package/src/grid/EnumPicker.tsx +108 -0
- package/src/grid/GridHeaderCell.tsx +243 -0
- package/src/grid/RecordView.tsx +293 -0
- package/src/grid/ValueDock.tsx +52 -0
- package/src/grid/aggregates.ts +94 -0
- package/src/grid/cell-range.ts +26 -0
- package/src/grid/column-layout.ts +71 -0
- package/src/grid/column-selection.ts +42 -0
- package/src/grid/column-view.ts +35 -0
- package/src/grid/copy-formats.ts +108 -0
- package/src/grid/display.ts +109 -0
- package/src/grid/edit-value.ts +60 -0
- package/src/grid/enum-type.ts +62 -0
- package/src/grid/focused-value.ts +23 -0
- package/src/grid/layout.ts +91 -0
- package/src/grid/navigation.ts +74 -0
- package/src/grid/row-selection.ts +35 -0
- package/src/grid/sort.ts +12 -0
- package/src/grid/types.ts +59 -0
- package/src/host/helper-path.ts +41 -0
- package/src/host/host.ts +456 -0
- package/src/host/index.ts +5 -0
- package/src/host/spawn.ts +124 -0
- package/src/host/types.ts +46 -0
- package/src/host/validate.ts +402 -0
- package/src/index.ts +37 -0
- package/src/locales/en.json +577 -0
- package/src/locales/nl.json +577 -0
- package/src/locales.ts +18 -0
- package/src/protocol/connection.ts +86 -0
- package/src/protocol/errors.ts +38 -0
- package/src/protocol/index.ts +33 -0
- package/src/protocol/messages.ts +168 -0
- package/src/protocol/schema.ts +76 -0
- package/src/protocol/values.ts +52 -0
- package/src/sql-split.ts +220 -0
- package/src/sql.ts +152 -0
- package/src/structure/StructureView.tsx +269 -0
- package/src/structure/structure-text.ts +4 -0
- package/src/table/CommandField.tsx +318 -0
- package/src/table/ImportDialog.tsx +168 -0
- package/src/table/TableStatusBar.tsx +91 -0
- package/src/table/TableToolbar.tsx +217 -0
- package/src/table/TableView.tsx +880 -0
- package/src/table/command-field.ts +285 -0
- package/src/table/grid-rows.ts +80 -0
- package/src/table/import-mapping.ts +37 -0
- package/src/table/layout-store.ts +62 -0
- package/src/table/paging.ts +33 -0
- package/src/table/pending.ts +143 -0
- package/src/table/references.ts +38 -0
- package/src/table/useLoaded.ts +58 -0
- package/src/table/useStoredLayout.ts +53 -0
- package/src/table/useTableTransfer.ts +187 -0
- package/src/testing/fake.ts +533 -0
- package/src/testing/index.ts +1 -0
- package/src/use-popup-press.ts +27 -0
- package/src/use-schema-change.ts +19 -0
- package/src/use-stable-callback.ts +12 -0
- package/src/value/ValuePanel.tsx +254 -0
- package/src/value/draft.ts +72 -0
- package/src/value/hex.ts +62 -0
- package/src/value/json.ts +74 -0
- package/src/value/views.ts +68 -0
- package/src/workbench/DatabaseWorkbench.tsx +348 -0
- package/src/workbench/DesignerTab.tsx +37 -0
- package/src/workbench/TableTab.tsx +27 -0
- package/src/workbench/tabs.ts +184 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { columnSql, defaultClause } from './column.ts';
|
|
2
|
+
import { createIndexSql, createTableParts } from './create.ts';
|
|
3
|
+
import { atLeast, quote, tableName, type Dialect } from './dialect.ts';
|
|
4
|
+
import type { TableDiff } from './diff.ts';
|
|
5
|
+
import { isAutoIndex, type ColumnDraft, type TableDraft } from './draft.ts';
|
|
6
|
+
import { dropIndexSql } from './statements.ts';
|
|
7
|
+
|
|
8
|
+
const RENAME_COLUMN = [3, 25];
|
|
9
|
+
const DROP_COLUMN = [3, 35];
|
|
10
|
+
|
|
11
|
+
/* The clauses `ALTER TABLE .. ADD COLUMN` refuses: a key, a computed value, or a NOT NULL column with nothing to fill the rows with. */
|
|
12
|
+
const addable = (column: ColumnDraft): boolean => {
|
|
13
|
+
if (column.autoIncrement || (column.generated && column.generatedClause !== null && /\bSTORED\b/i.test(column.generatedClause))) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
const expression = column.defaultValue?.trim() ?? '';
|
|
17
|
+
if (expression === '') {
|
|
18
|
+
return column.nullable;
|
|
19
|
+
}
|
|
20
|
+
// Only a constant may fill existing rows: not an expression, not the clock.
|
|
21
|
+
const clause = defaultClause(expression);
|
|
22
|
+
return !clause.startsWith('(') && !/^CURRENT_|^LOCALTIME|^NOW/i.test(clause) && (column.nullable || !/^NULL$/i.test(clause));
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const needsRebuild = (dialect: Dialect, original: TableDraft, diff: TableDiff): boolean => {
|
|
26
|
+
if (diff.optionsChanged || diff.primaryKeyChanged || diff.droppedForeignKeys.length > 0 || diff.addedForeignKeys.length > 0) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
if (diff.keptColumns.some((kept) => kept.redefined || kept.moved || (kept.renamed && !atLeast(dialect, ...RENAME_COLUMN)))) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
if (diff.addedColumns.some((added) => added.placed || !addable(added.column))) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
if (diff.droppedColumns.length > 0 && !atLeast(dialect, ...DROP_COLUMN)) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
const autoIndexed = new Set(original.indexes.filter((index) => isAutoIndex(index.name)).flatMap((index) => index.columns));
|
|
39
|
+
if (diff.droppedColumns.some((dropped) => autoIndexed.has(dropped.name))) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
return [...diff.droppedIndexes, ...diff.renamedIndexes.map(({ from }) => from)].some((index) => isAutoIndex(index.name));
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/* The steps of https://www.sqlite.org/lang_altertable.html under "Making Other Kinds Of Table Schema Changes", which keep the rows, the indexes and the foreign keys of other tables intact. */
|
|
46
|
+
const rebuildSql = (dialect: Dialect, schema: string, original: TableDraft, draft: TableDraft, diff: TableDiff): string[] => {
|
|
47
|
+
const temporary = `new_${draft.name}`;
|
|
48
|
+
const { table, indexes } = createTableParts(dialect, schema, draft, temporary);
|
|
49
|
+
const copied = diff.keptColumns.filter((kept) => !kept.column.generated);
|
|
50
|
+
const statements = ['PRAGMA foreign_keys=OFF', 'BEGIN', table];
|
|
51
|
+
if (copied.length > 0) {
|
|
52
|
+
const into = copied.map((kept) => quote(dialect, kept.column.name)).join(', ');
|
|
53
|
+
const from = copied.map((kept) => quote(dialect, kept.original.name)).join(', ');
|
|
54
|
+
statements.push(`INSERT INTO ${tableName(dialect, schema, temporary)} (${into}) SELECT ${from} FROM ${tableName(dialect, schema, original.name)}`);
|
|
55
|
+
}
|
|
56
|
+
statements.push(
|
|
57
|
+
`DROP TABLE ${tableName(dialect, schema, original.name)}`,
|
|
58
|
+
`ALTER TABLE ${tableName(dialect, schema, temporary)} RENAME TO ${quote(dialect, draft.name)}`,
|
|
59
|
+
...indexes.map((index) => createIndexSql(dialect, schema, draft.name, index)),
|
|
60
|
+
'PRAGMA foreign_key_check',
|
|
61
|
+
'COMMIT',
|
|
62
|
+
'PRAGMA foreign_keys=ON'
|
|
63
|
+
);
|
|
64
|
+
return statements;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/* `ALTER TABLE` where SQLite has one, a rebuild of the table for every other change. */
|
|
68
|
+
export const sqliteAlterSql = (dialect: Dialect, schema: string, original: TableDraft, draft: TableDraft, diff: TableDiff): string[] => {
|
|
69
|
+
if (needsRebuild(dialect, original, diff)) {
|
|
70
|
+
return rebuildSql(dialect, schema, original, draft, diff);
|
|
71
|
+
}
|
|
72
|
+
const target = tableName(dialect, schema, original.name);
|
|
73
|
+
const statements: string[] = [];
|
|
74
|
+
for (const index of [...diff.droppedIndexes, ...diff.renamedIndexes.map(({ from }) => from)]) {
|
|
75
|
+
statements.push(dropIndexSql(dialect, schema, original.name, index.name));
|
|
76
|
+
}
|
|
77
|
+
for (const dropped of diff.droppedColumns) {
|
|
78
|
+
statements.push(`ALTER TABLE ${target} DROP COLUMN ${quote(dialect, dropped.name)}`);
|
|
79
|
+
}
|
|
80
|
+
for (const kept of diff.keptColumns.filter((candidate) => candidate.renamed)) {
|
|
81
|
+
statements.push(`ALTER TABLE ${target} RENAME COLUMN ${quote(dialect, kept.original.name)} TO ${quote(dialect, kept.column.name)}`);
|
|
82
|
+
}
|
|
83
|
+
for (const added of diff.addedColumns) {
|
|
84
|
+
statements.push(`ALTER TABLE ${target} ADD COLUMN ${columnSql(dialect, added.column)}`);
|
|
85
|
+
}
|
|
86
|
+
for (const index of [...diff.addedIndexes, ...diff.renamedIndexes.map(({ to }) => to)]) {
|
|
87
|
+
statements.push(createIndexSql(dialect, schema, original.name, index));
|
|
88
|
+
}
|
|
89
|
+
if (diff.renamed) {
|
|
90
|
+
statements.push(`ALTER TABLE ${target} RENAME TO ${quote(dialect, draft.name)}`);
|
|
91
|
+
}
|
|
92
|
+
return statements;
|
|
93
|
+
};
|
package/src/ddl/alter.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { TableStructure } from '../protocol/index.ts';
|
|
2
|
+
import { mysqlAlterSql } from './alter-mysql.ts';
|
|
3
|
+
import { sqliteAlterSql } from './alter-sqlite.ts';
|
|
4
|
+
import type { Dialect } from './dialect.ts';
|
|
5
|
+
import { diffOf, type TableDiff } from './diff.ts';
|
|
6
|
+
import { draftOf, type TableDraft } from './draft.ts';
|
|
7
|
+
|
|
8
|
+
const unchanged = (diff: TableDiff): boolean =>
|
|
9
|
+
!diff.renamed &&
|
|
10
|
+
!diff.optionsChanged &&
|
|
11
|
+
!diff.primaryKeyChanged &&
|
|
12
|
+
diff.droppedColumns.length === 0 &&
|
|
13
|
+
diff.addedColumns.length === 0 &&
|
|
14
|
+
diff.keptColumns.every((kept) => !kept.renamed && !kept.redefined && !kept.moved) &&
|
|
15
|
+
diff.droppedIndexes.length === 0 &&
|
|
16
|
+
diff.addedIndexes.length === 0 &&
|
|
17
|
+
diff.renamedIndexes.length === 0 &&
|
|
18
|
+
diff.droppedForeignKeys.length === 0 &&
|
|
19
|
+
diff.addedForeignKeys.length === 0;
|
|
20
|
+
|
|
21
|
+
/* The fewest statements that turn the table of `structure` into the draft; none when the draft changes nothing. */
|
|
22
|
+
export const alterTableSql = (dialect: Dialect, schema: string, structure: TableStructure, draft: TableDraft): string[] => {
|
|
23
|
+
const original = draftOf(structure);
|
|
24
|
+
const diff = diffOf(dialect, original, draft);
|
|
25
|
+
if (unchanged(diff)) {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
return dialect.engine === 'sqlite' ? sqliteAlterSql(dialect, schema, original, draft, diff) : mysqlAlterSql(dialect, schema, original, draft, diff);
|
|
29
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { sqlLiteral } from '../sql.ts';
|
|
2
|
+
import { quote, type Dialect } from './dialect.ts';
|
|
3
|
+
import type { ColumnDraft } from './draft.ts';
|
|
4
|
+
|
|
5
|
+
const NUMBER = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
|
|
6
|
+
const STRING = /^'(?:[^']|'')*'$/;
|
|
7
|
+
const KEYWORD = /^(?:NULL|TRUE|FALSE|CURRENT_DATE|CURRENT_TIME(?:\(\d*\))?|CURRENT_TIMESTAMP(?:\(\d*\))?|LOCALTIME(?:STAMP)?(?:\(\d*\))?|NOW\(\d*\))$/i;
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
* The text after `DEFAULT`. A literal and the few keywords both engines take stand as written; any
|
|
11
|
+
* other expression goes in parentheses, which SQLite and MySQL 8 need for one that is not constant.
|
|
12
|
+
*/
|
|
13
|
+
export const defaultClause = (expression: string): string => {
|
|
14
|
+
const text = expression.trim();
|
|
15
|
+
if (NUMBER.test(text) || STRING.test(text) || KEYWORD.test(text)) {
|
|
16
|
+
return text;
|
|
17
|
+
}
|
|
18
|
+
return text.startsWith('(') && text.endsWith(')') ? text : `(${text})`;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export interface ColumnSqlOptions {
|
|
22
|
+
/* SQLite only: the column is the whole primary key, which an `AUTOINCREMENT` has to be declared on. */
|
|
23
|
+
readonly inlinePrimaryKey?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* A column's definition without its name: `varchar(255) NOT NULL DEFAULT 'x'`. */
|
|
27
|
+
export const columnSpecSql = (dialect: Dialect, column: ColumnDraft, { inlinePrimaryKey = false }: ColumnSqlOptions = {}): string => {
|
|
28
|
+
const mysql = dialect.engine === 'mysql';
|
|
29
|
+
const parts: string[] = [];
|
|
30
|
+
if (column.type !== '') {
|
|
31
|
+
parts.push(column.type);
|
|
32
|
+
}
|
|
33
|
+
if (column.generated && column.generatedClause !== null) {
|
|
34
|
+
parts.push(column.generatedClause);
|
|
35
|
+
}
|
|
36
|
+
if (inlinePrimaryKey) {
|
|
37
|
+
parts.push(column.autoIncrement ? 'PRIMARY KEY AUTOINCREMENT' : 'PRIMARY KEY');
|
|
38
|
+
}
|
|
39
|
+
// MariaDB takes neither NULL nor NOT NULL after the expression of a computed column.
|
|
40
|
+
if (column.generated && dialect.flavor === 'mariadb') {
|
|
41
|
+
// The expression says everything.
|
|
42
|
+
} else if (!column.nullable) {
|
|
43
|
+
parts.push('NOT NULL');
|
|
44
|
+
} else if (mysql) {
|
|
45
|
+
// MySQL gives a TIMESTAMP column NOT NULL unless it is told otherwise.
|
|
46
|
+
parts.push('NULL');
|
|
47
|
+
}
|
|
48
|
+
if (!column.generated) {
|
|
49
|
+
if (column.defaultValue !== null && column.defaultValue.trim() !== '') {
|
|
50
|
+
parts.push(`DEFAULT ${defaultClause(column.defaultValue)}`);
|
|
51
|
+
}
|
|
52
|
+
if (mysql && column.autoIncrement) {
|
|
53
|
+
parts.push('AUTO_INCREMENT');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (mysql && column.comment !== '') {
|
|
57
|
+
parts.push(`COMMENT ${sqlLiteral('mysql', column.comment)}`);
|
|
58
|
+
}
|
|
59
|
+
return parts.join(' ');
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const columnSql = (dialect: Dialect, column: ColumnDraft, options?: ColumnSqlOptions): string =>
|
|
63
|
+
`${quote(dialect, column.name)} ${columnSpecSql(dialect, column, options)}`.trimEnd();
|
|
64
|
+
|
|
65
|
+
const typeKey = (type: string): string => type.toLowerCase().replace(/\s+/g, ' ').trim();
|
|
66
|
+
|
|
67
|
+
/* Whether two columns are defined alike, whatever their names. SQLite keeps no comments, so they never differ there. */
|
|
68
|
+
export const sameDefinition = (dialect: Dialect, left: ColumnDraft, right: ColumnDraft): boolean =>
|
|
69
|
+
typeKey(left.type) === typeKey(right.type) &&
|
|
70
|
+
left.nullable === right.nullable &&
|
|
71
|
+
(left.defaultValue ?? '').trim() === (right.defaultValue ?? '').trim() &&
|
|
72
|
+
left.autoIncrement === right.autoIncrement &&
|
|
73
|
+
(dialect.engine === 'sqlite' || left.comment === right.comment) &&
|
|
74
|
+
left.generatedClause === right.generatedClause;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { sqlLiteral } from '../sql.ts';
|
|
2
|
+
import { columnSql } from './column.ts';
|
|
3
|
+
import { columnList, quote, sqliteSchemaPrefix, tableName, type Dialect } from './dialect.ts';
|
|
4
|
+
import { isAutoIndex, type ForeignKeyDraft, type IndexDraft, type TableDraft } from './draft.ts';
|
|
5
|
+
import type { TableOptions } from './parse.ts';
|
|
6
|
+
|
|
7
|
+
const INDENT = ' ';
|
|
8
|
+
|
|
9
|
+
/* `CONSTRAINT name FOREIGN KEY (...) REFERENCES t (...) ON UPDATE .. ON DELETE ..`, shared by a create and an `ADD`. */
|
|
10
|
+
export const foreignKeySql = (dialect: Dialect, schema: string, foreignKey: ForeignKeyDraft): string => {
|
|
11
|
+
const sqlite = dialect.engine === 'sqlite';
|
|
12
|
+
// SQLite cannot name another schema in a reference, and MySQL needs one only for another database.
|
|
13
|
+
const target =
|
|
14
|
+
sqlite || foreignKey.referencedSchema === '' || foreignKey.referencedSchema === schema
|
|
15
|
+
? quote(dialect, foreignKey.referencedTable)
|
|
16
|
+
: `${quote(dialect, foreignKey.referencedSchema)}.${quote(dialect, foreignKey.referencedTable)}`;
|
|
17
|
+
const parts = [
|
|
18
|
+
...(foreignKey.name === '' ? [] : [`CONSTRAINT ${quote(dialect, foreignKey.name)}`]),
|
|
19
|
+
`FOREIGN KEY (${columnList(dialect, foreignKey.columns)})`,
|
|
20
|
+
`REFERENCES ${target} (${columnList(dialect, foreignKey.referencedColumns)})`
|
|
21
|
+
];
|
|
22
|
+
if (foreignKey.onUpdate !== null) {
|
|
23
|
+
parts.push(`ON UPDATE ${foreignKey.onUpdate}`);
|
|
24
|
+
}
|
|
25
|
+
if (foreignKey.onDelete !== null) {
|
|
26
|
+
parts.push(`ON DELETE ${foreignKey.onDelete}`);
|
|
27
|
+
}
|
|
28
|
+
return parts.join(' ');
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/* `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ...`: the options that say something, and with `against` only those that differ from it. */
|
|
32
|
+
export const mysqlOptionsSql = (options: TableOptions, against?: TableOptions): string[] => {
|
|
33
|
+
// Only a comment can be emptied; an engine, a character set or a collation has no empty value to ask for.
|
|
34
|
+
const changed = (field: 'engine' | 'charset' | 'collation' | 'comment'): boolean =>
|
|
35
|
+
options[field] !== against?.[field] && (options[field] !== '' || (field === 'comment' && against !== undefined));
|
|
36
|
+
return [
|
|
37
|
+
...(changed('engine') ? [`ENGINE=${options.engine}`] : []),
|
|
38
|
+
...(changed('charset') ? [`DEFAULT CHARSET=${options.charset}`] : []),
|
|
39
|
+
...(changed('collation') ? [`COLLATE=${options.collation}`] : []),
|
|
40
|
+
...(changed('comment') ? [`COMMENT=${sqlLiteral('mysql', options.comment)}`] : [])
|
|
41
|
+
];
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const sqliteOptionsSql = (options: TableOptions): string[] => [...(options.strict ? ['STRICT'] : []), ...(options.withoutRowid ? ['WITHOUT ROWID'] : [])];
|
|
45
|
+
|
|
46
|
+
const inlineIndexSql = (dialect: Dialect, index: IndexDraft): string =>
|
|
47
|
+
[index.unique ? 'UNIQUE INDEX' : 'INDEX', ...(index.name === '' ? [] : [quote(dialect, index.name)]), `(${columnList(dialect, index.columns)})`].join(' ');
|
|
48
|
+
|
|
49
|
+
/* `CREATE [UNIQUE] INDEX name ON table (columns)`. */
|
|
50
|
+
export const createIndexSql = (dialect: Dialect, schema: string, table: string, index: Pick<IndexDraft, 'name' | 'columns' | 'unique'>): string =>
|
|
51
|
+
`CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${dialect.engine === 'sqlite' ? sqliteSchemaPrefix(dialect, schema) : ''}${quote(dialect, index.name)} ON ${tableName(dialect, dialect.engine === 'sqlite' ? '' : schema, table)} (${columnList(dialect, index.columns)})`;
|
|
52
|
+
|
|
53
|
+
export interface CreateParts {
|
|
54
|
+
readonly table: string;
|
|
55
|
+
/* SQLite creates its indexes after the table, where MySQL declares them inside it. */
|
|
56
|
+
readonly indexes: readonly IndexDraft[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* 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
|
+
export const createTableParts = (dialect: Dialect, schema: string, draft: TableDraft, name: string): CreateParts => {
|
|
61
|
+
const sqlite = dialect.engine === 'sqlite';
|
|
62
|
+
// SQLite declares AUTOINCREMENT on the column itself, which then is the whole primary key; a WITHOUT ROWID table has no rowid to count.
|
|
63
|
+
const autoColumn =
|
|
64
|
+
sqlite && draft.primaryKey.length === 1 && !draft.options.withoutRowid
|
|
65
|
+
? draft.columns.find((column) => column.name === draft.primaryKey[0] && column.autoIncrement)?.name
|
|
66
|
+
: undefined;
|
|
67
|
+
const lines = draft.columns.map((column) => columnSql(dialect, column, { inlinePrimaryKey: column.name === autoColumn }));
|
|
68
|
+
if (draft.primaryKey.length > 0 && autoColumn === undefined) {
|
|
69
|
+
lines.push(`PRIMARY KEY (${columnList(dialect, draft.primaryKey)})`);
|
|
70
|
+
}
|
|
71
|
+
const separate: IndexDraft[] = [];
|
|
72
|
+
for (const index of draft.indexes) {
|
|
73
|
+
if (!sqlite) {
|
|
74
|
+
lines.push(inlineIndexSql(dialect, index));
|
|
75
|
+
} else if (isAutoIndex(index.name) && index.unique) {
|
|
76
|
+
lines.push(`UNIQUE (${columnList(dialect, index.columns)})`);
|
|
77
|
+
} else if (!isAutoIndex(index.name)) {
|
|
78
|
+
separate.push(index);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
for (const foreignKey of draft.foreignKeys) {
|
|
82
|
+
lines.push(foreignKeySql(dialect, schema, foreignKey));
|
|
83
|
+
}
|
|
84
|
+
const options = sqlite ? sqliteOptionsSql(draft.options).join(', ') : mysqlOptionsSql(draft.options).join(' ');
|
|
85
|
+
const head = `CREATE TABLE ${tableName(dialect, schema, name)} (\n${lines.map((line) => INDENT + line).join(',\n')}\n)`;
|
|
86
|
+
return { table: options === '' ? head : `${head} ${options}`, indexes: separate };
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const createTableSql = (dialect: Dialect, schema: string, draft: TableDraft): string[] => {
|
|
90
|
+
const { table, indexes } = createTableParts(dialect, schema, draft, draft.name);
|
|
91
|
+
return [table, ...indexes.map((index) => createIndexSql(dialect, schema, draft.name, index))];
|
|
92
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Engine, ServerInfo } from '../protocol/index.ts';
|
|
2
|
+
import { qualifiedName, quoteIdentifier } from '../sql.ts';
|
|
3
|
+
|
|
4
|
+
/* The SQL a server speaks: the engine picks the quoting and the shape of a statement, the version what that server can do without a rebuild. */
|
|
5
|
+
export interface Dialect {
|
|
6
|
+
readonly flavor: ServerInfo['flavor'];
|
|
7
|
+
readonly engine: Engine;
|
|
8
|
+
/* The numbers of the version, such as `[3, 45, 1]`; empty when the server's text holds none. */
|
|
9
|
+
readonly version: readonly number[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const VERSION = /(\d+(?:\.\d+)*)/;
|
|
13
|
+
|
|
14
|
+
export const dialectOf = (server: ServerInfo): Dialect => ({
|
|
15
|
+
flavor: server.flavor,
|
|
16
|
+
engine: server.flavor === 'sqlite' ? 'sqlite' : 'mysql',
|
|
17
|
+
version: (VERSION.exec(server.version)?.[1] ?? '')
|
|
18
|
+
.split('.')
|
|
19
|
+
.filter((part) => part !== '')
|
|
20
|
+
.map(Number)
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/* Whether the server is at least this version. A version nobody could read counts as new enough. */
|
|
24
|
+
export const atLeast = (dialect: Dialect, ...wanted: number[]): boolean => {
|
|
25
|
+
if (dialect.version.length === 0) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
for (let i = 0; i < wanted.length; i++) {
|
|
29
|
+
const have = dialect.version[i] ?? 0;
|
|
30
|
+
if (have !== wanted[i]) {
|
|
31
|
+
return have > wanted[i]!;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return true;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const quote = (dialect: Dialect, name: string): string => quoteIdentifier(dialect.engine, name);
|
|
38
|
+
|
|
39
|
+
/* SQLite reads an unqualified name as `main`, so only an attached schema is spelled out. */
|
|
40
|
+
const schemaOf = (dialect: Dialect, schema: string): string | undefined =>
|
|
41
|
+
schema === '' || (dialect.engine === 'sqlite' && schema === 'main') ? undefined : schema;
|
|
42
|
+
|
|
43
|
+
/* A table with its schema in front, when the statement needs one. */
|
|
44
|
+
export const tableName = (dialect: Dialect, schema: string, table: string): string =>
|
|
45
|
+
qualifiedName({ engine: dialect.engine, table, schema: schemaOf(dialect, schema) });
|
|
46
|
+
|
|
47
|
+
/* SQLite puts the schema in front of the name of an index, where MySQL names the table it is on. */
|
|
48
|
+
export const sqliteSchemaPrefix = (dialect: Dialect, schema: string): string => {
|
|
49
|
+
const named = schemaOf(dialect, schema);
|
|
50
|
+
return named === undefined ? '' : `${quote(dialect, named)}.`;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const columnList = (dialect: Dialect, columns: readonly string[]): string => columns.map((column) => quote(dialect, column)).join(', ');
|
package/src/ddl/diff.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { sameDefinition } from './column.ts';
|
|
2
|
+
import type { Dialect } from './dialect.ts';
|
|
3
|
+
import type { ColumnDraft, ForeignKeyDraft, IndexDraft, TableDraft } from './draft.ts';
|
|
4
|
+
import type { TableOptions } from './parse.ts';
|
|
5
|
+
|
|
6
|
+
export interface KeptColumn {
|
|
7
|
+
readonly original: ColumnDraft;
|
|
8
|
+
readonly column: ColumnDraft;
|
|
9
|
+
readonly renamed: boolean;
|
|
10
|
+
readonly redefined: boolean;
|
|
11
|
+
/* It sits elsewhere among the columns that stay, which only a position clause can say. */
|
|
12
|
+
readonly moved: boolean;
|
|
13
|
+
/* The column before it in the draft, or `null` when it is the first. */
|
|
14
|
+
readonly after: string | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface AddedColumn {
|
|
18
|
+
readonly column: ColumnDraft;
|
|
19
|
+
/* Whether a column that stays comes after it, so that it cannot simply go to the end. */
|
|
20
|
+
readonly placed: boolean;
|
|
21
|
+
readonly after: string | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface RenamedIndex {
|
|
25
|
+
readonly from: IndexDraft;
|
|
26
|
+
readonly to: IndexDraft;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface TableDiff {
|
|
30
|
+
readonly renamed: boolean;
|
|
31
|
+
readonly droppedColumns: readonly ColumnDraft[];
|
|
32
|
+
readonly keptColumns: readonly KeptColumn[];
|
|
33
|
+
readonly addedColumns: readonly AddedColumn[];
|
|
34
|
+
readonly primaryKeyChanged: boolean;
|
|
35
|
+
/* As the original named them: an index that is gone and one that has to be made again. */
|
|
36
|
+
readonly droppedIndexes: readonly IndexDraft[];
|
|
37
|
+
readonly addedIndexes: readonly IndexDraft[];
|
|
38
|
+
readonly renamedIndexes: readonly RenamedIndex[];
|
|
39
|
+
readonly droppedForeignKeys: readonly ForeignKeyDraft[];
|
|
40
|
+
readonly addedForeignKeys: readonly ForeignKeyDraft[];
|
|
41
|
+
readonly optionsChanged: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const sameList = (left: readonly string[], right: readonly string[]): boolean => left.length === right.length && left.every((name, at) => name === right[at]);
|
|
45
|
+
|
|
46
|
+
/* The positions of a longest run that already is in order: those columns can stay where they are. */
|
|
47
|
+
const longestRun = (positions: readonly number[]): ReadonlySet<number> => {
|
|
48
|
+
const length: number[] = [];
|
|
49
|
+
const previous: number[] = [];
|
|
50
|
+
let best = -1;
|
|
51
|
+
for (let i = 0; i < positions.length; i++) {
|
|
52
|
+
length[i] = 1;
|
|
53
|
+
previous[i] = -1;
|
|
54
|
+
for (let before = 0; before < i; before++) {
|
|
55
|
+
if (positions[before]! < positions[i]! && length[before]! + 1 > length[i]!) {
|
|
56
|
+
length[i] = length[before]! + 1;
|
|
57
|
+
previous[i] = before;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (best < 0 || length[i]! > length[best]!) {
|
|
61
|
+
best = i;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const run = new Set<number>();
|
|
65
|
+
for (let at = best; at >= 0; at = previous[at]!) {
|
|
66
|
+
run.add(at);
|
|
67
|
+
}
|
|
68
|
+
return run;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const OPTION_FIELDS: readonly (keyof TableOptions)[] = ['engine', 'charset', 'collation', 'comment', 'withoutRowid', 'strict'];
|
|
72
|
+
|
|
73
|
+
const matches = (original: { readonly name: string; readonly key: string }, draft: { readonly originalName: string | null; readonly key: string }): boolean =>
|
|
74
|
+
draft.originalName !== null && (original.name === '' ? draft.key === original.key : draft.originalName === original.name);
|
|
75
|
+
|
|
76
|
+
/* What a draft changes in the table it came from, told apart by the names items kept. */
|
|
77
|
+
export const diffOf = (dialect: Dialect, original: TableDraft, draft: TableDraft): TableDiff => {
|
|
78
|
+
const survivors = draft.columns.filter((column) => column.originalName !== null);
|
|
79
|
+
const renames = new Map(survivors.map((column) => [column.originalName!, column.name]));
|
|
80
|
+
const project = (names: readonly string[]): string[] => names.map((name) => renames.get(name) ?? name);
|
|
81
|
+
const originalOf = (name: string): ColumnDraft | undefined => original.columns.find((column) => column.name === name);
|
|
82
|
+
|
|
83
|
+
const positions = survivors.map((column) => original.columns.findIndex((candidate) => candidate.name === column.originalName));
|
|
84
|
+
const staying = longestRun(positions);
|
|
85
|
+
let survivor = 0;
|
|
86
|
+
const keptColumns: KeptColumn[] = [];
|
|
87
|
+
const addedColumns: AddedColumn[] = [];
|
|
88
|
+
draft.columns.forEach((column, at) => {
|
|
89
|
+
const after = at === 0 ? null : draft.columns[at - 1]!.name;
|
|
90
|
+
if (column.originalName === null) {
|
|
91
|
+
addedColumns.push({ column, placed: draft.columns.slice(at + 1).some((later) => later.originalName !== null), after });
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const rank = survivor++;
|
|
95
|
+
const before = originalOf(column.originalName);
|
|
96
|
+
if (before === undefined) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
keptColumns.push({
|
|
100
|
+
original: before,
|
|
101
|
+
column,
|
|
102
|
+
renamed: before.name !== column.name,
|
|
103
|
+
redefined: !sameDefinition(dialect, before, column),
|
|
104
|
+
moved: !staying.has(rank),
|
|
105
|
+
after
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const droppedColumns = original.columns.filter((column) => !survivors.some((kept) => kept.originalName === column.name));
|
|
110
|
+
|
|
111
|
+
const droppedIndexes: IndexDraft[] = [];
|
|
112
|
+
const addedIndexes = draft.indexes.filter((index) => index.originalName === null);
|
|
113
|
+
const renamedIndexes: RenamedIndex[] = [];
|
|
114
|
+
for (const before of original.indexes) {
|
|
115
|
+
const after = draft.indexes.find((index) => matches(before, index));
|
|
116
|
+
if (after === undefined) {
|
|
117
|
+
droppedIndexes.push(before);
|
|
118
|
+
} else if (!sameList(project(before.columns), after.columns) || before.unique !== after.unique) {
|
|
119
|
+
droppedIndexes.push(before);
|
|
120
|
+
addedIndexes.push(after);
|
|
121
|
+
} else if (before.name !== after.name) {
|
|
122
|
+
renamedIndexes.push({ from: before, to: after });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const droppedForeignKeys: ForeignKeyDraft[] = [];
|
|
127
|
+
const addedForeignKeys = draft.foreignKeys.filter((key) => key.originalName === null);
|
|
128
|
+
for (const before of original.foreignKeys) {
|
|
129
|
+
const after = draft.foreignKeys.find((key) => matches(before, key));
|
|
130
|
+
const same =
|
|
131
|
+
after !== undefined &&
|
|
132
|
+
before.name === after.name &&
|
|
133
|
+
sameList(project(before.columns), after.columns) &&
|
|
134
|
+
before.referencedSchema === after.referencedSchema &&
|
|
135
|
+
before.referencedTable === after.referencedTable &&
|
|
136
|
+
sameList(before.referencedColumns, after.referencedColumns) &&
|
|
137
|
+
before.onUpdate === after.onUpdate &&
|
|
138
|
+
before.onDelete === after.onDelete;
|
|
139
|
+
if (!same) {
|
|
140
|
+
droppedForeignKeys.push(before);
|
|
141
|
+
if (after !== undefined) {
|
|
142
|
+
addedForeignKeys.push(after);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
renamed: original.name !== draft.name,
|
|
149
|
+
droppedColumns,
|
|
150
|
+
keptColumns,
|
|
151
|
+
addedColumns,
|
|
152
|
+
primaryKeyChanged: !sameList(project(original.primaryKey), draft.primaryKey),
|
|
153
|
+
droppedIndexes,
|
|
154
|
+
addedIndexes,
|
|
155
|
+
renamedIndexes,
|
|
156
|
+
droppedForeignKeys,
|
|
157
|
+
addedForeignKeys,
|
|
158
|
+
optionsChanged: OPTION_FIELDS.some((field) => original.options[field] !== draft.options[field])
|
|
159
|
+
};
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/* Whether applying the draft removes a column, and with it the data in it. */
|
|
163
|
+
export const dropsColumns = (original: TableDraft, draft: TableDraft): boolean =>
|
|
164
|
+
original.columns.some((column) => !draft.columns.some((kept) => kept.originalName === column.name));
|
package/src/ddl/draft.ts
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type { ForeignKeyInfo, TableStructure } from '../protocol/index.ts';
|
|
2
|
+
import { generatedClausesOf, NO_OPTIONS, optionsOf, type TableOptions } from './parse.ts';
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Every item keeps the name it had in the structure it came from, so a rename is told apart from a
|
|
6
|
+
* drop plus an add. `originalName` is `null` for an item the person added. `key` is stable while
|
|
7
|
+
* the item is edited, which a name is not.
|
|
8
|
+
*/
|
|
9
|
+
export interface ColumnDraft {
|
|
10
|
+
readonly key: string;
|
|
11
|
+
readonly originalName: string | null;
|
|
12
|
+
readonly name: string;
|
|
13
|
+
/* The type as typed, such as `varchar(255)`; empty for an SQLite column without one. */
|
|
14
|
+
readonly type: string;
|
|
15
|
+
readonly nullable: boolean;
|
|
16
|
+
/* An SQL expression such as `'draft'` or `CURRENT_TIMESTAMP`; `null` is no default. */
|
|
17
|
+
readonly defaultValue: string | null;
|
|
18
|
+
readonly autoIncrement: boolean;
|
|
19
|
+
/* Empty for no comment. */
|
|
20
|
+
readonly comment: string;
|
|
21
|
+
/* A column the server computes. It is read only here: its clause travels along unchanged. */
|
|
22
|
+
readonly generated: boolean;
|
|
23
|
+
/* `GENERATED ALWAYS AS (...) STORED` as the table's DDL says it; `null` when the DDL did not show it. */
|
|
24
|
+
readonly generatedClause: string | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface IndexDraft {
|
|
28
|
+
readonly key: string;
|
|
29
|
+
readonly originalName: string | null;
|
|
30
|
+
readonly name: string;
|
|
31
|
+
readonly columns: readonly string[];
|
|
32
|
+
readonly unique: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ForeignKeyDraft {
|
|
36
|
+
readonly key: string;
|
|
37
|
+
/* An SQLite foreign key without a name has the empty string; `null` is a key the person added. */
|
|
38
|
+
readonly originalName: string | null;
|
|
39
|
+
readonly name: string;
|
|
40
|
+
readonly columns: readonly string[];
|
|
41
|
+
readonly referencedSchema: string;
|
|
42
|
+
readonly referencedTable: string;
|
|
43
|
+
readonly referencedColumns: readonly string[];
|
|
44
|
+
/* `null` is the engine's default. */
|
|
45
|
+
readonly onUpdate: string | null;
|
|
46
|
+
readonly onDelete: string | null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface TableDraft {
|
|
50
|
+
/* `null` for a table that does not exist yet. */
|
|
51
|
+
readonly originalName: string | null;
|
|
52
|
+
readonly name: string;
|
|
53
|
+
readonly columns: readonly ColumnDraft[];
|
|
54
|
+
/* Names of columns of the draft, in key order. */
|
|
55
|
+
readonly primaryKey: readonly string[];
|
|
56
|
+
/* Without the primary key, which has its own field. */
|
|
57
|
+
readonly indexes: readonly IndexDraft[];
|
|
58
|
+
readonly foreignKeys: readonly ForeignKeyDraft[];
|
|
59
|
+
readonly options: TableOptions;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let counter = 0;
|
|
63
|
+
|
|
64
|
+
/* A key no other item of this page has. */
|
|
65
|
+
export const nextKey = (): string => `new-${++counter}`;
|
|
66
|
+
|
|
67
|
+
export const emptyColumn = (name = ''): ColumnDraft => ({
|
|
68
|
+
key: nextKey(),
|
|
69
|
+
originalName: null,
|
|
70
|
+
name,
|
|
71
|
+
type: '',
|
|
72
|
+
nullable: true,
|
|
73
|
+
defaultValue: null,
|
|
74
|
+
autoIncrement: false,
|
|
75
|
+
comment: '',
|
|
76
|
+
generated: false,
|
|
77
|
+
generatedClause: null
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
export const emptyDraft = (): TableDraft => ({
|
|
81
|
+
originalName: null,
|
|
82
|
+
name: '',
|
|
83
|
+
columns: [],
|
|
84
|
+
primaryKey: [],
|
|
85
|
+
indexes: [],
|
|
86
|
+
foreignKeys: [],
|
|
87
|
+
options: NO_OPTIONS
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
/* SQLite names the index behind a `UNIQUE` or `PRIMARY KEY` constraint itself; such an index cannot be created or dropped by name. */
|
|
91
|
+
export const isAutoIndex = (name: string): boolean => name.startsWith('sqlite_autoindex_');
|
|
92
|
+
|
|
93
|
+
const foreignKeyDraftOf = (foreignKey: ForeignKeyInfo, at: number): ForeignKeyDraft => ({
|
|
94
|
+
key: `foreign-key:${at}`,
|
|
95
|
+
originalName: foreignKey.name ?? '',
|
|
96
|
+
name: foreignKey.name ?? '',
|
|
97
|
+
columns: foreignKey.columns,
|
|
98
|
+
referencedSchema: foreignKey.referencedSchema,
|
|
99
|
+
referencedTable: foreignKey.referencedTable,
|
|
100
|
+
referencedColumns: foreignKey.referencedColumns,
|
|
101
|
+
onUpdate: foreignKey.onUpdate,
|
|
102
|
+
onDelete: foreignKey.onDelete
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
/* The draft that changes nothing: what a structure already says. */
|
|
106
|
+
export const draftOf = (structure: TableStructure): TableDraft => {
|
|
107
|
+
const clauses = generatedClausesOf(structure.ddl);
|
|
108
|
+
return {
|
|
109
|
+
originalName: structure.name,
|
|
110
|
+
name: structure.name,
|
|
111
|
+
columns: structure.columns.map((column) => ({
|
|
112
|
+
key: `column:${column.name}`,
|
|
113
|
+
originalName: column.name,
|
|
114
|
+
name: column.name,
|
|
115
|
+
type: column.type,
|
|
116
|
+
nullable: column.nullable,
|
|
117
|
+
defaultValue: column.defaultValue,
|
|
118
|
+
autoIncrement: column.autoIncrement,
|
|
119
|
+
comment: column.comment ?? '',
|
|
120
|
+
generated: column.generated,
|
|
121
|
+
generatedClause: column.generated ? (clauses.get(column.name) ?? null) : null
|
|
122
|
+
})),
|
|
123
|
+
primaryKey: structure.primaryKey,
|
|
124
|
+
indexes: structure.indexes
|
|
125
|
+
.filter((index) => !index.primary)
|
|
126
|
+
.map((index) => ({
|
|
127
|
+
key: `index:${index.name}`,
|
|
128
|
+
originalName: index.name,
|
|
129
|
+
name: index.name,
|
|
130
|
+
columns: index.columns,
|
|
131
|
+
unique: index.unique
|
|
132
|
+
})),
|
|
133
|
+
foreignKeys: structure.foreignKeys.map(foreignKeyDraftOf),
|
|
134
|
+
options: optionsOf(structure)
|
|
135
|
+
};
|
|
136
|
+
};
|
package/src/ddl/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { alterTableSql } from './alter.ts';
|
|
2
|
+
export { columnSpecSql, defaultClause } from './column.ts';
|
|
3
|
+
export { createIndexSql, createTableSql } from './create.ts';
|
|
4
|
+
export { atLeast, dialectOf, type Dialect } from './dialect.ts';
|
|
5
|
+
export { diffOf, dropsColumns, type TableDiff } from './diff.ts';
|
|
6
|
+
export { draftOf, emptyColumn, emptyDraft, isAutoIndex, nextKey, type ColumnDraft, type ForeignKeyDraft, type IndexDraft, type TableDraft } from './draft.ts';
|
|
7
|
+
export { NO_OPTIONS, optionsOf, type TableOptions } from './parse.ts';
|
|
8
|
+
export { dropIndexSql, dropTableSql, dropViewSql, renameTableSql, truncateTableSql } from './statements.ts';
|
|
9
|
+
export { FOREIGN_KEY_ACTIONS, MYSQL_CHARSETS, MYSQL_ENGINES, collationSuggestionsOf, typeSuggestionsOf } from './types.ts';
|
|
10
|
+
export { validateDraft, type DraftProblem } from './validate.ts';
|