@adecore/database 0.38.0 → 0.40.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/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/ddl/parse.ts
CHANGED
|
@@ -13,8 +13,10 @@ interface Spot {
|
|
|
13
13
|
function* unquoted(text: string, backslash: boolean): Generator<Spot> {
|
|
14
14
|
let depth = 0;
|
|
15
15
|
let closer: string | null = null;
|
|
16
|
+
|
|
16
17
|
for (let i = 0; i < text.length; i++) {
|
|
17
18
|
const char = text[i]!;
|
|
19
|
+
|
|
18
20
|
if (closer !== null) {
|
|
19
21
|
if (backslash && char === '\\' && closer === "'") {
|
|
20
22
|
i++;
|
|
@@ -25,16 +27,22 @@ function* unquoted(text: string, backslash: boolean): Generator<Spot> {
|
|
|
25
27
|
closer = null;
|
|
26
28
|
}
|
|
27
29
|
}
|
|
30
|
+
|
|
28
31
|
continue;
|
|
29
32
|
}
|
|
33
|
+
|
|
30
34
|
if (char in CLOSERS) {
|
|
31
35
|
closer = CLOSERS[char]!;
|
|
36
|
+
|
|
32
37
|
continue;
|
|
33
38
|
}
|
|
39
|
+
|
|
34
40
|
if (char === ')') {
|
|
35
41
|
depth--;
|
|
36
42
|
}
|
|
43
|
+
|
|
37
44
|
yield { index: i, char, depth };
|
|
45
|
+
|
|
38
46
|
if (char === '(') {
|
|
39
47
|
depth++;
|
|
40
48
|
}
|
|
@@ -53,11 +61,13 @@ export const definitionOf = (ddl: string | null): Definition | null => {
|
|
|
53
61
|
if (ddl === null) {
|
|
54
62
|
return null;
|
|
55
63
|
}
|
|
64
|
+
|
|
56
65
|
// Only a MySQL statement reads a backslash as an escape, and only a MySQL statement quotes with backticks.
|
|
57
66
|
const backslash = ddl.includes('`');
|
|
58
67
|
let open = -1;
|
|
59
68
|
let close = -1;
|
|
60
69
|
const commas: number[] = [];
|
|
70
|
+
|
|
61
71
|
for (const { index, char, depth } of unquoted(ddl, backslash)) {
|
|
62
72
|
if (open < 0) {
|
|
63
73
|
if (char === '(' && depth === 0) {
|
|
@@ -71,10 +81,13 @@ export const definitionOf = (ddl: string | null): Definition | null => {
|
|
|
71
81
|
}
|
|
72
82
|
}
|
|
73
83
|
}
|
|
84
|
+
|
|
74
85
|
if (open < 0 || close < 0) {
|
|
75
86
|
return null;
|
|
76
87
|
}
|
|
88
|
+
|
|
77
89
|
const bounds = [open, ...commas, close];
|
|
90
|
+
|
|
78
91
|
return {
|
|
79
92
|
items: bounds.slice(1).map((end, at) => ddl.slice(bounds[at]! + 1, end).trim()),
|
|
80
93
|
tail: ddl.slice(close + 1)
|
|
@@ -84,14 +97,19 @@ export const definitionOf = (ddl: string | null): Definition | null => {
|
|
|
84
97
|
/* The name a definition starts with, unquoted. */
|
|
85
98
|
const leadingName = (item: string): string | null => {
|
|
86
99
|
const mark = item[0];
|
|
100
|
+
|
|
87
101
|
if (mark === '`' || mark === '"') {
|
|
88
102
|
const end = item.indexOf(mark, 1);
|
|
103
|
+
|
|
89
104
|
return end < 0 ? null : item.slice(1, end).replaceAll(mark + mark, mark);
|
|
90
105
|
}
|
|
106
|
+
|
|
91
107
|
if (mark === '[') {
|
|
92
108
|
const end = item.indexOf(']');
|
|
109
|
+
|
|
93
110
|
return end < 0 ? null : item.slice(1, end);
|
|
94
111
|
}
|
|
112
|
+
|
|
95
113
|
return /^[^\s(]+/.exec(item)?.[0] ?? null;
|
|
96
114
|
};
|
|
97
115
|
|
|
@@ -103,34 +121,44 @@ const generatedClauseOf = (item: string): string | null => {
|
|
|
103
121
|
// A default or a comment may hold the words, so the strings are blanked before the search.
|
|
104
122
|
const masked = item.replace(/'(?:[^']|'')*'/g, (text) => ' '.repeat(text.length));
|
|
105
123
|
const start = AS_OPEN.exec(masked);
|
|
124
|
+
|
|
106
125
|
if (start === null) {
|
|
107
126
|
return null;
|
|
108
127
|
}
|
|
128
|
+
|
|
109
129
|
const open = start.index + start[0].length - 1;
|
|
110
130
|
let end = -1;
|
|
131
|
+
|
|
111
132
|
for (const { index, char, depth } of unquoted(masked.slice(open), false)) {
|
|
112
133
|
if (char === ')' && depth === 0) {
|
|
113
134
|
end = open + index + 1;
|
|
135
|
+
|
|
114
136
|
break;
|
|
115
137
|
}
|
|
116
138
|
}
|
|
139
|
+
|
|
117
140
|
if (end < 0) {
|
|
118
141
|
return null;
|
|
119
142
|
}
|
|
143
|
+
|
|
120
144
|
const suffix = AS_SUFFIX.exec(masked.slice(end));
|
|
145
|
+
|
|
121
146
|
return item.slice(start.index, end + (suffix?.[0].length ?? 0)).trim();
|
|
122
147
|
};
|
|
123
148
|
|
|
124
149
|
/* The generated clause of every generated column in a table's DDL, by column name. */
|
|
125
150
|
export const generatedClausesOf = (ddl: string | null): ReadonlyMap<string, string> => {
|
|
126
151
|
const clauses = new Map<string, string>();
|
|
152
|
+
|
|
127
153
|
for (const item of definitionOf(ddl)?.items ?? []) {
|
|
128
154
|
const name = leadingName(item);
|
|
129
155
|
const clause = name === null ? null : generatedClauseOf(item);
|
|
156
|
+
|
|
130
157
|
if (name !== null && clause !== null) {
|
|
131
158
|
clauses.set(name, clause);
|
|
132
159
|
}
|
|
133
160
|
}
|
|
161
|
+
|
|
134
162
|
return clauses;
|
|
135
163
|
};
|
|
136
164
|
|
|
@@ -148,8 +176,11 @@ export const NO_OPTIONS: TableOptions = { engine: '', charset: '', collation: ''
|
|
|
148
176
|
|
|
149
177
|
export const optionsOf = (structure: Pick<TableStructure, 'ddl'>): TableOptions => {
|
|
150
178
|
const tail = definitionOf(structure.ddl)?.tail ?? '';
|
|
179
|
+
|
|
151
180
|
const word = (pattern: RegExp): string => pattern.exec(tail)?.[1] ?? '';
|
|
181
|
+
|
|
152
182
|
const comment = /\bCOMMENT\s*=?\s*'((?:[^'\\]|''|\\.)*)'/i.exec(tail)?.[1] ?? '';
|
|
183
|
+
|
|
153
184
|
return {
|
|
154
185
|
engine: word(/\bENGINE\s*=\s*(\w+)/i),
|
|
155
186
|
charset: word(/\b(?:DEFAULT\s+)?(?:CHARSET|CHARACTER\s+SET)\s*=?\s*(\w+)/i),
|
package/src/ddl/types.ts
CHANGED
|
@@ -50,10 +50,13 @@ export const collationSuggestionsOf = (charset: string): readonly string[] => {
|
|
|
50
50
|
if (charset === '') {
|
|
51
51
|
return [];
|
|
52
52
|
}
|
|
53
|
+
|
|
53
54
|
if (charset === 'binary') {
|
|
54
55
|
return ['binary'];
|
|
55
56
|
}
|
|
57
|
+
|
|
56
58
|
const suggestions = [`${charset}_general_ci`, `${charset}_unicode_ci`, `${charset}_bin`];
|
|
59
|
+
|
|
57
60
|
return charset === 'utf8mb4' ? ['utf8mb4_0900_ai_ci', 'utf8mb4_unicode_520_ci', ...suggestions] : suggestions;
|
|
58
61
|
};
|
|
59
62
|
|
package/src/ddl/validate.ts
CHANGED
|
@@ -9,39 +9,51 @@ export interface DraftProblem {
|
|
|
9
9
|
|
|
10
10
|
export const validateDraft = (dialect: Dialect, draft: TableDraft): DraftProblem[] => {
|
|
11
11
|
const problems: DraftProblem[] = [];
|
|
12
|
+
|
|
12
13
|
if (draft.name.trim() === '') {
|
|
13
14
|
problems.push({ code: 'tableName', subject: '' });
|
|
14
15
|
}
|
|
16
|
+
|
|
15
17
|
if (draft.columns.length === 0) {
|
|
16
18
|
problems.push({ code: 'noColumns', subject: '' });
|
|
17
19
|
}
|
|
20
|
+
|
|
18
21
|
const seen = new Set<string>();
|
|
22
|
+
|
|
19
23
|
for (const column of draft.columns) {
|
|
20
24
|
const folded = column.name.toLowerCase();
|
|
25
|
+
|
|
21
26
|
if (column.name.trim() === '') {
|
|
22
27
|
problems.push({ code: 'columnName', subject: '' });
|
|
23
28
|
} else if (seen.has(folded)) {
|
|
24
29
|
problems.push({ code: 'columnDuplicate', subject: column.name });
|
|
25
30
|
}
|
|
31
|
+
|
|
26
32
|
seen.add(folded);
|
|
33
|
+
|
|
27
34
|
if (dialect.engine === 'mysql' && column.type.trim() === '' && column.name.trim() !== '') {
|
|
28
35
|
problems.push({ code: 'columnType', subject: column.name });
|
|
29
36
|
}
|
|
30
37
|
}
|
|
38
|
+
|
|
31
39
|
for (const index of draft.indexes) {
|
|
32
40
|
if (index.columns.length === 0) {
|
|
33
41
|
problems.push({ code: 'indexColumns', subject: index.name });
|
|
34
42
|
}
|
|
43
|
+
|
|
35
44
|
if (dialect.engine === 'sqlite' && index.name.trim() === '') {
|
|
36
45
|
problems.push({ code: 'indexName', subject: '' });
|
|
37
46
|
}
|
|
38
47
|
}
|
|
48
|
+
|
|
39
49
|
for (const foreignKey of draft.foreignKeys) {
|
|
40
50
|
const complete =
|
|
41
51
|
foreignKey.columns.length > 0 && foreignKey.referencedTable !== '' && foreignKey.referencedColumns.length === foreignKey.columns.length;
|
|
52
|
+
|
|
42
53
|
if (!complete) {
|
|
43
54
|
problems.push({ code: 'foreignKey', subject: foreignKey.name });
|
|
44
55
|
}
|
|
45
56
|
}
|
|
57
|
+
|
|
46
58
|
return problems;
|
|
47
59
|
};
|
|
@@ -17,6 +17,7 @@ export function ColumnsEditor({ draft, dialect, disabled, onChange, className, r
|
|
|
17
17
|
const listId = useId();
|
|
18
18
|
const mysql = dialect.engine === 'mysql';
|
|
19
19
|
const defaultType = typeSuggestionsOf(dialect)[0]!;
|
|
20
|
+
|
|
20
21
|
return (
|
|
21
22
|
<div ref={ref} className={clsx('flex min-h-0 flex-col', className)}>
|
|
22
23
|
{draft.columns.length === 0 ? (
|
|
@@ -103,7 +104,9 @@ function ColumnRow({ draft, column, at, mysql, listId, disabled, onChange }: Col
|
|
|
103
104
|
const { t } = useTranslation('database');
|
|
104
105
|
const keyAt = draft.primaryKey.indexOf(column.name);
|
|
105
106
|
const locked = disabled || column.generated;
|
|
107
|
+
|
|
106
108
|
const patch = (change: Partial<ColumnDraft>): void => onChange(patchColumn(draft, column.key, change));
|
|
109
|
+
|
|
107
110
|
return (
|
|
108
111
|
<tr>
|
|
109
112
|
<td className={clsx(TD, 'whitespace-nowrap')}>
|
|
@@ -73,12 +73,14 @@ export function DesignerView({
|
|
|
73
73
|
}: DesignerViewProps) {
|
|
74
74
|
const { t } = useTranslation('database');
|
|
75
75
|
const disabled = readOnlyReason !== null;
|
|
76
|
+
|
|
76
77
|
const applyReason =
|
|
77
78
|
readOnlyReason ?? (plan.problems.length > 0 ? t('designer.fixProblems') : plan.statements.length === 0 ? t('designer.noChanges') : null);
|
|
78
79
|
|
|
79
80
|
function handleKeyDown(event: KeyboardEvent<HTMLDivElement>): void {
|
|
80
81
|
if (matchesShortcut(APPLY_SHORTCUT, event.nativeEvent, isApplePlatform())) {
|
|
81
82
|
event.preventDefault();
|
|
83
|
+
|
|
82
84
|
if (applyReason === null) {
|
|
83
85
|
onRequestApply();
|
|
84
86
|
}
|
|
@@ -22,18 +22,22 @@ const DEFAULT_ACTION = 'default';
|
|
|
22
22
|
export function ForeignKeysEditor({ draft, schema, tables, referenceColumns, disabled, onChange, className, ref }: ForeignKeysEditorProps) {
|
|
23
23
|
const { t } = useTranslation('database');
|
|
24
24
|
const columns = draft.columns.map((column) => column.name);
|
|
25
|
+
|
|
25
26
|
const actions = [
|
|
26
27
|
{ value: DEFAULT_ACTION, label: t('designer.foreignKeys.engineDefault') },
|
|
27
28
|
...FOREIGN_KEY_ACTIONS.map((action) => ({ value: action, label: action }))
|
|
28
29
|
];
|
|
30
|
+
|
|
29
31
|
return (
|
|
30
32
|
<div ref={ref} className={clsx('flex min-h-0 flex-col', className)}>
|
|
31
33
|
<div>
|
|
32
34
|
{draft.foreignKeys.length === 0 && <EmptyState>{t('designer.foreignKeys.empty')}</EmptyState>}
|
|
33
35
|
{draft.foreignKeys.map((foreignKey) => {
|
|
34
36
|
const referenced = foreignKey.referencedTable === '' ? [] : referenceColumns(foreignKey.referencedTable);
|
|
37
|
+
|
|
35
38
|
const choices =
|
|
36
39
|
tables.includes(foreignKey.referencedTable) || foreignKey.referencedTable === '' ? tables : [...tables, foreignKey.referencedTable];
|
|
40
|
+
|
|
37
41
|
return (
|
|
38
42
|
<div key={foreignKey.key} className="flex flex-col gap-3 border-b border-border-soft px-3 py-3">
|
|
39
43
|
<div className="flex items-end gap-3">
|
|
@@ -10,6 +10,7 @@ import { addIndex, patchIndex, removeIndex, toggleIndexColumn } from './edit.ts'
|
|
|
10
10
|
export function IndexesEditor({ draft, disabled, onChange, className, ref }: EditorProps) {
|
|
11
11
|
const { t } = useTranslation('database');
|
|
12
12
|
const columns = draft.columns.map((column) => column.name);
|
|
13
|
+
|
|
13
14
|
return (
|
|
14
15
|
<div ref={ref} className={clsx('flex min-h-0 flex-col', className)}>
|
|
15
16
|
<div>
|
|
@@ -20,11 +20,13 @@ export interface NamePickerProps {
|
|
|
20
20
|
/* Picks some of a few names, such as the columns of an index, and numbers the picks once there is more than one. */
|
|
21
21
|
export function NamePicker({ options, value, onToggle, label, disabled = false, className, ref }: NamePickerProps) {
|
|
22
22
|
const { t } = useTranslation('database');
|
|
23
|
+
|
|
23
24
|
return (
|
|
24
25
|
<div ref={ref} role="group" aria-label={label} className={clsx('flex flex-wrap items-center gap-1', className)}>
|
|
25
26
|
{options.length === 0 && <span className="text-xs text-text-faint">{t('designer.noColumnsToPick')}</span>}
|
|
26
27
|
{options.map((name) => {
|
|
27
28
|
const position = value.indexOf(name);
|
|
29
|
+
|
|
28
30
|
return (
|
|
29
31
|
<Button
|
|
30
32
|
key={name}
|
|
@@ -11,7 +11,9 @@ export function OptionsEditor({ draft, dialect, disabled, onChange, className, r
|
|
|
11
11
|
const { t } = useTranslation('database');
|
|
12
12
|
const id = useId();
|
|
13
13
|
const { options } = draft;
|
|
14
|
+
|
|
14
15
|
const patch = (change: Parameters<typeof patchOptions>[1]): void => onChange(patchOptions(draft, change));
|
|
16
|
+
|
|
15
17
|
if (dialect.engine === 'sqlite') {
|
|
16
18
|
return (
|
|
17
19
|
<div ref={ref} className={clsx('flex flex-col gap-4 p-3', className)}>
|
|
@@ -32,6 +34,7 @@ export function OptionsEditor({ draft, dialect, disabled, onChange, className, r
|
|
|
32
34
|
</div>
|
|
33
35
|
);
|
|
34
36
|
}
|
|
37
|
+
|
|
35
38
|
return (
|
|
36
39
|
<div ref={ref} className={clsx('flex max-w-xl flex-col gap-4 p-3', className)}>
|
|
37
40
|
<SuggestedField
|
|
@@ -106,6 +109,7 @@ function SuggestedField({
|
|
|
106
109
|
onValueChange(value: string): void;
|
|
107
110
|
}) {
|
|
108
111
|
const { t } = useTranslation('database');
|
|
112
|
+
|
|
109
113
|
return (
|
|
110
114
|
<>
|
|
111
115
|
<datalist id={listId}>
|
|
@@ -16,6 +16,7 @@ export interface SqlPreviewProps {
|
|
|
16
16
|
/* The sentence a problem of the draft is read as. */
|
|
17
17
|
function ProblemText({ problem }: { problem: DraftProblem }) {
|
|
18
18
|
const { t } = useTranslation('database');
|
|
19
|
+
|
|
19
20
|
return <>{t(`ddl.problem.${problem.code}`, { subject: problem.subject })}</>;
|
|
20
21
|
}
|
|
21
22
|
|
|
@@ -23,6 +24,7 @@ function ProblemText({ problem }: { problem: DraftProblem }) {
|
|
|
23
24
|
export function SqlPreview({ plan, className, ref }: SqlPreviewProps) {
|
|
24
25
|
const { t } = useTranslation('database');
|
|
25
26
|
const script = scriptOf(plan.statements);
|
|
27
|
+
|
|
26
28
|
return (
|
|
27
29
|
<section
|
|
28
30
|
ref={ref}
|
|
@@ -59,15 +59,18 @@ export function TableDesigner({ connection, schema, table, toolbarStart, classNa
|
|
|
59
59
|
const loaded = useLoaded<Snapshot>(
|
|
60
60
|
async (signal) => {
|
|
61
61
|
const session = client.session(connection);
|
|
62
|
+
|
|
62
63
|
const [server, structure, tables] = await Promise.all([
|
|
63
64
|
session.server({ signal }),
|
|
64
65
|
subject.current === null ? Promise.resolve(null) : session.structure(schema, subject.current, { signal }),
|
|
65
66
|
session.tables(schema, { signal })
|
|
66
67
|
]);
|
|
68
|
+
|
|
67
69
|
return { dialect: dialectOf(server), structure, tables: tables.filter((info) => info.kind === 'table').map((info) => info.name) };
|
|
68
70
|
},
|
|
69
71
|
JSON.stringify([connection.id, schema, subject.current])
|
|
70
72
|
);
|
|
73
|
+
|
|
71
74
|
const snapshot = loaded.value;
|
|
72
75
|
|
|
73
76
|
// The apply of this designer reloads by itself; a change from elsewhere replaces an untouched draft and warns about an edited one.
|
|
@@ -89,6 +92,7 @@ export function TableDesigner({ connection, schema, table, toolbarStart, classNa
|
|
|
89
92
|
setEditing({ snapshot, baseline, draft: baseline });
|
|
90
93
|
setShapeChanged(false);
|
|
91
94
|
}
|
|
95
|
+
|
|
92
96
|
const draft = editing.draft;
|
|
93
97
|
|
|
94
98
|
const plan = useMemo(() => (snapshot === null ? null : planOf(snapshot.dialect, schema, snapshot.structure, draft)), [snapshot, schema, draft]);
|
|
@@ -101,30 +105,40 @@ export function TableDesigner({ connection, schema, table, toolbarStart, classNa
|
|
|
101
105
|
if (snapshot === null || plan === null) {
|
|
102
106
|
return;
|
|
103
107
|
}
|
|
108
|
+
|
|
104
109
|
const session = client.session(connection);
|
|
105
110
|
const statements = plan.statements;
|
|
106
111
|
const wasNew = snapshot.structure === null;
|
|
112
|
+
|
|
107
113
|
const settle = (message: string | null): void => {
|
|
108
114
|
setFailure(message);
|
|
109
115
|
setConfirming(false);
|
|
110
116
|
};
|
|
117
|
+
|
|
111
118
|
let outcome: ExecuteResult;
|
|
112
119
|
applying.current = true;
|
|
120
|
+
|
|
113
121
|
try {
|
|
114
122
|
outcome = await session.execute(statements.join(';\n'), { schema });
|
|
115
123
|
} catch (e) {
|
|
116
124
|
settle(messageOf(e, t('designer.failed')));
|
|
125
|
+
|
|
117
126
|
return;
|
|
118
127
|
} finally {
|
|
119
128
|
applying.current = false;
|
|
120
129
|
}
|
|
130
|
+
|
|
121
131
|
const failed = outcome.results.find((result) => result.kind === 'error');
|
|
132
|
+
|
|
122
133
|
if (failed?.kind === 'error') {
|
|
123
134
|
await recoverFrom(session, statements, failed.sql, outcome.inTransaction);
|
|
124
135
|
settle(failed.error.message);
|
|
136
|
+
|
|
125
137
|
return;
|
|
126
138
|
}
|
|
139
|
+
|
|
127
140
|
settle(null);
|
|
141
|
+
|
|
128
142
|
// The client has told its listeners already: `execute` does for a statement that creates, alters or drops.
|
|
129
143
|
if (wasNew || draft.name !== draft.originalName) {
|
|
130
144
|
setSubject({ requested: table, current: draft.name });
|
package/src/designer/edit.ts
CHANGED
|
@@ -18,13 +18,17 @@ export const addColumn = (draft: TableDraft, type: string): TableDraft => ({
|
|
|
18
18
|
/* A rename carries along every key, index and foreign key that names the column. */
|
|
19
19
|
export const patchColumn = (draft: TableDraft, key: string, patch: Partial<ColumnDraft>): TableDraft => {
|
|
20
20
|
const before = draft.columns.find((column) => column.key === key);
|
|
21
|
+
|
|
21
22
|
if (before === undefined) {
|
|
22
23
|
return draft;
|
|
23
24
|
}
|
|
25
|
+
|
|
24
26
|
const inKey = draft.primaryKey.includes(before.name);
|
|
25
27
|
const applied: Partial<ColumnDraft> = inKey && patch.nullable === true ? { ...patch, nullable: false } : patch;
|
|
26
28
|
const after = { ...before, ...applied };
|
|
29
|
+
|
|
27
30
|
const follow = (names: readonly string[]): string[] => names.map((name) => (name === before.name ? after.name : name));
|
|
31
|
+
|
|
28
32
|
return {
|
|
29
33
|
...draft,
|
|
30
34
|
columns: replaceAt(draft.columns, key, () => after),
|
|
@@ -37,9 +41,11 @@ export const patchColumn = (draft: TableDraft, key: string, patch: Partial<Colum
|
|
|
37
41
|
/* What names the column goes with it: an index keeps its other columns, a foreign key on it is dropped. */
|
|
38
42
|
export const removeColumn = (draft: TableDraft, key: string): TableDraft => {
|
|
39
43
|
const gone = draft.columns.find((column) => column.key === key);
|
|
44
|
+
|
|
40
45
|
if (gone === undefined) {
|
|
41
46
|
return draft;
|
|
42
47
|
}
|
|
48
|
+
|
|
43
49
|
return {
|
|
44
50
|
...draft,
|
|
45
51
|
columns: draft.columns.filter((column) => column.key !== key),
|
|
@@ -54,22 +60,28 @@ export const removeColumn = (draft: TableDraft, key: string): TableDraft => {
|
|
|
54
60
|
export const moveColumn = (draft: TableDraft, key: string, offset: -1 | 1): TableDraft => {
|
|
55
61
|
const from = draft.columns.findIndex((column) => column.key === key);
|
|
56
62
|
const to = from + offset;
|
|
63
|
+
|
|
57
64
|
if (from < 0 || to < 0 || to >= draft.columns.length) {
|
|
58
65
|
return draft;
|
|
59
66
|
}
|
|
67
|
+
|
|
60
68
|
const columns = [...draft.columns];
|
|
61
69
|
[columns[from], columns[to]] = [columns[to]!, columns[from]!];
|
|
70
|
+
|
|
62
71
|
return { ...draft, columns };
|
|
63
72
|
};
|
|
64
73
|
|
|
65
74
|
/* A column in the primary key cannot be null, so putting it there says so. */
|
|
66
75
|
export const togglePrimaryKey = (draft: TableDraft, key: string): TableDraft => {
|
|
67
76
|
const column = draft.columns.find((candidate) => candidate.key === key);
|
|
77
|
+
|
|
68
78
|
if (column === undefined) {
|
|
69
79
|
return draft;
|
|
70
80
|
}
|
|
81
|
+
|
|
71
82
|
const joining = !draft.primaryKey.includes(column.name);
|
|
72
83
|
const toggledKey = { ...draft, primaryKey: toggled(draft.primaryKey, column.name) };
|
|
84
|
+
|
|
73
85
|
return joining ? patchColumn(toggledKey, key, { nullable: false }) : toggledKey;
|
|
74
86
|
};
|
|
75
87
|
|
|
@@ -133,5 +145,6 @@ export const removeForeignKey = (draft: TableDraft, key: string): TableDraft =>
|
|
|
133
145
|
/* A collation belongs to a character set, so choosing another set empties it. */
|
|
134
146
|
export const patchOptions = (draft: TableDraft, patch: Partial<TableOptions>): TableDraft => {
|
|
135
147
|
const charsetChanged = patch.charset !== undefined && patch.charset !== draft.options.charset;
|
|
148
|
+
|
|
136
149
|
return { ...draft, options: { ...draft.options, ...(charsetChanged ? { collation: '' } : {}), ...patch } };
|
|
137
150
|
};
|
package/src/designer/plan.ts
CHANGED
|
@@ -12,12 +12,15 @@ export interface Plan {
|
|
|
12
12
|
|
|
13
13
|
export const planOf = (dialect: Dialect, schema: string, structure: TableStructure | null, draft: TableDraft): Plan => {
|
|
14
14
|
const problems = validateDraft(dialect, draft);
|
|
15
|
+
|
|
15
16
|
if (problems.length > 0) {
|
|
16
17
|
return { problems, statements: [], destructive: false };
|
|
17
18
|
}
|
|
19
|
+
|
|
18
20
|
if (structure === null) {
|
|
19
21
|
return { problems, statements: createTableSql(dialect, schema, draft), destructive: false };
|
|
20
22
|
}
|
|
23
|
+
|
|
21
24
|
return { problems, statements: alterTableSql(dialect, schema, structure, draft), destructive: dropsColumns(draftOf(structure), draft) };
|
|
22
25
|
};
|
|
23
26
|
|
package/src/designer/recover.ts
CHANGED
|
@@ -11,6 +11,7 @@ export async function recoverFrom(session: DatabaseSession, statements: readonly
|
|
|
11
11
|
if (inTransaction && statements.includes('BEGIN') && failedSql.trim().toUpperCase() !== 'BEGIN') {
|
|
12
12
|
await session.transaction('rollback');
|
|
13
13
|
}
|
|
14
|
+
|
|
14
15
|
if (statements[0] === REBUILD_START) {
|
|
15
16
|
await session.execute('PRAGMA foreign_keys=ON');
|
|
16
17
|
}
|
|
@@ -21,17 +21,21 @@ export function useReferenceColumns(
|
|
|
21
21
|
const scope = `${connection.id}\u0000${schema}`;
|
|
22
22
|
const [cache, setCache] = useState<Cache>({ scope, columns: {} });
|
|
23
23
|
const known = cache.scope === scope ? cache.columns : {};
|
|
24
|
+
|
|
24
25
|
const wanted = [...new Set(draft.foreignKeys.map((foreignKey) => foreignKey.referencedTable))]
|
|
25
26
|
.filter((table) => table !== '' && table !== draft.name && !(table in known))
|
|
26
27
|
.sort();
|
|
28
|
+
|
|
27
29
|
const wantedKey = wanted.join('\u0000');
|
|
28
30
|
|
|
29
31
|
useEffect(() => {
|
|
30
32
|
if (wantedKey === '') {
|
|
31
33
|
return undefined;
|
|
32
34
|
}
|
|
35
|
+
|
|
33
36
|
let current = true;
|
|
34
37
|
const session = client.session(connection);
|
|
38
|
+
|
|
35
39
|
for (const table of wantedKey.split('\u0000')) {
|
|
36
40
|
session
|
|
37
41
|
.structure(schema, table)
|
|
@@ -45,6 +49,7 @@ export function useReferenceColumns(
|
|
|
45
49
|
}
|
|
46
50
|
});
|
|
47
51
|
}
|
|
52
|
+
|
|
48
53
|
return () => {
|
|
49
54
|
current = false;
|
|
50
55
|
};
|