@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.
Files changed (131) hide show
  1. package/dist/console/QueryConsole.js +2 -10
  2. package/dist/console/SchemaPicker.js +0 -1
  3. package/dist/console/StatementResultView.js +1 -1
  4. package/dist/ddl/diff.js +1 -1
  5. package/dist/explorer/tree.js +0 -1
  6. package/dist/export-formats.d.ts +2 -0
  7. package/dist/export-formats.js +1 -0
  8. package/dist/grid/CopyAsMenu.js +0 -1
  9. package/dist/grid/DataGrid.js +10 -10
  10. package/dist/grid/column-layout.d.ts +0 -1
  11. package/dist/grid/column-layout.js +1 -2
  12. package/dist/grid/column-view.js +3 -2
  13. package/dist/grid/display.js +1 -2
  14. package/dist/host/host.js +1 -2
  15. package/dist/host/validate.d.ts +3 -0
  16. package/dist/host/validate.js +18 -15
  17. package/dist/same-list.d.ts +1 -0
  18. package/dist/same-list.js +1 -0
  19. package/dist/stored-size.d.ts +1 -0
  20. package/dist/stored-size.js +10 -0
  21. package/dist/table/TableToolbar.js +1 -1
  22. package/dist/table/TableView.js +2 -10
  23. package/package.json +15 -15
  24. package/src/DatabaseProvider.tsx +2 -0
  25. package/src/KeyIcon.tsx +1 -0
  26. package/src/client/client.ts +13 -0
  27. package/src/client-context.ts +2 -0
  28. package/src/connections/ConnectionForm.tsx +11 -0
  29. package/src/connections/ConnectionManager.tsx +9 -0
  30. package/src/connections/connection-config.ts +21 -0
  31. package/src/connections/docker.ts +2 -0
  32. package/src/connections/use-docker-containers.ts +3 -0
  33. package/src/console/DestructiveDialog.tsx +1 -0
  34. package/src/console/QueryConsole.tsx +38 -10
  35. package/src/console/SchemaPicker.tsx +1 -1
  36. package/src/console/SqlEditor.tsx +10 -0
  37. package/src/console/StatementResultView.tsx +7 -2
  38. package/src/console/TransactionControls.tsx +1 -0
  39. package/src/console/destructive.ts +11 -0
  40. package/src/console/editing.ts +10 -0
  41. package/src/console/history.ts +8 -0
  42. package/src/console/labels.ts +1 -0
  43. package/src/console/outcome.ts +1 -0
  44. package/src/console/result-keys.ts +16 -0
  45. package/src/console/shortcuts.ts +2 -0
  46. package/src/ddl/alter-mysql.ts +16 -0
  47. package/src/ddl/alter-sqlite.ts +21 -0
  48. package/src/ddl/alter.ts +2 -0
  49. package/src/ddl/column.ts +11 -0
  50. package/src/ddl/create.ts +15 -0
  51. package/src/ddl/dialect.ts +4 -0
  52. package/src/ddl/diff.ts +22 -2
  53. package/src/ddl/draft.ts +1 -0
  54. package/src/ddl/parse.ts +31 -0
  55. package/src/ddl/types.ts +3 -0
  56. package/src/ddl/validate.ts +12 -0
  57. package/src/designer/ColumnsEditor.tsx +3 -0
  58. package/src/designer/DesignerView.tsx +2 -0
  59. package/src/designer/ForeignKeysEditor.tsx +4 -0
  60. package/src/designer/IndexesEditor.tsx +1 -0
  61. package/src/designer/NamePicker.tsx +2 -0
  62. package/src/designer/OptionsEditor.tsx +4 -0
  63. package/src/designer/SqlPreview.tsx +2 -0
  64. package/src/designer/TableDesigner.tsx +14 -0
  65. package/src/designer/edit.ts +13 -0
  66. package/src/designer/plan.ts +3 -0
  67. package/src/designer/recover.ts +1 -0
  68. package/src/designer/useReferenceColumns.ts +5 -0
  69. package/src/explorer/DatabaseExplorer.tsx +40 -0
  70. package/src/explorer/RowMenu.tsx +2 -0
  71. package/src/explorer/TableDialog.tsx +6 -0
  72. package/src/explorer/expansion.ts +8 -0
  73. package/src/explorer/tree.ts +73 -2
  74. package/src/explorer/use-explorer-loads.ts +15 -0
  75. package/src/explorer/use-stored-expansion.ts +4 -0
  76. package/src/export-formats.ts +3 -0
  77. package/src/grid/CellEditor.tsx +1 -0
  78. package/src/grid/CopyAsMenu.tsx +1 -1
  79. package/src/grid/DataGrid.tsx +93 -23
  80. package/src/grid/EnumPicker.tsx +6 -0
  81. package/src/grid/GridHeaderCell.tsx +11 -0
  82. package/src/grid/GridRowView.tsx +3 -0
  83. package/src/grid/RecordView.tsx +19 -0
  84. package/src/grid/ValueDock.tsx +2 -0
  85. package/src/grid/aggregates.ts +12 -0
  86. package/src/grid/column-layout.ts +12 -4
  87. package/src/grid/column-selection.ts +9 -0
  88. package/src/grid/column-view.ts +6 -2
  89. package/src/grid/copy-formats.ts +13 -0
  90. package/src/grid/display.ts +17 -3
  91. package/src/grid/edit-value.ts +11 -0
  92. package/src/grid/enum-type.ts +15 -0
  93. package/src/grid/focused-value.ts +4 -0
  94. package/src/grid/layout.ts +12 -0
  95. package/src/grid/navigation.ts +5 -0
  96. package/src/grid/row-selection.ts +7 -0
  97. package/src/grid/sort.ts +1 -0
  98. package/src/grid/whole-cells.ts +8 -0
  99. package/src/host/host.ts +16 -3
  100. package/src/host/spawn.ts +2 -0
  101. package/src/host/validate.ts +37 -15
  102. package/src/same-list.ts +2 -0
  103. package/src/sql-split.ts +28 -0
  104. package/src/sql.ts +20 -0
  105. package/src/stored-size.ts +10 -0
  106. package/src/structure/StructureView.tsx +18 -0
  107. package/src/table/CommandField.tsx +11 -0
  108. package/src/table/ImportDialog.tsx +4 -0
  109. package/src/table/TableStatusBar.tsx +1 -0
  110. package/src/table/TableToolbar.tsx +2 -2
  111. package/src/table/TableView.tsx +50 -10
  112. package/src/table/command-field.ts +40 -0
  113. package/src/table/grid-rows.ts +13 -0
  114. package/src/table/import-mapping.ts +5 -0
  115. package/src/table/layout-store.ts +7 -0
  116. package/src/table/paging.ts +1 -0
  117. package/src/table/pending.ts +18 -0
  118. package/src/table/references.ts +4 -0
  119. package/src/table/useLoaded.ts +3 -0
  120. package/src/table/useStoredLayout.ts +2 -0
  121. package/src/table/useTableTransfer.ts +17 -1
  122. package/src/testing/fake.ts +24 -0
  123. package/src/use-database-resources.ts +1 -0
  124. package/src/use-popup-press.ts +3 -0
  125. package/src/value/ValueEditor.tsx +6 -0
  126. package/src/value/draft.ts +7 -0
  127. package/src/value/hex.ts +8 -0
  128. package/src/value/json.ts +16 -0
  129. package/src/value/views.ts +14 -0
  130. package/src/workbench/DatabaseWorkbench.tsx +22 -0
  131. 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
 
@@ -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 });
@@ -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
  };
@@ -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
 
@@ -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
  };