@adecore/database 0.21.0 → 0.21.2

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 (109) hide show
  1. package/dist/assign-ref.d.ts +2 -0
  2. package/dist/assign-ref.js +9 -0
  3. package/dist/client/client.js +14 -15
  4. package/dist/client/types.d.ts +1 -0
  5. package/dist/client/types.js +2 -0
  6. package/dist/connections/ConnectionForm.js +7 -2
  7. package/dist/connections/ConnectionManager.js +5 -5
  8. package/dist/connections/EngineIcon.js +0 -1
  9. package/dist/console/QueryConsole.js +12 -4
  10. package/dist/console/editing.js +2 -4
  11. package/dist/console/useConsoleHistory.js +3 -2
  12. package/dist/ddl/alter-mysql.js +2 -8
  13. package/dist/ddl/alter-sqlite.js +1 -1
  14. package/dist/ddl/column.js +5 -8
  15. package/dist/ddl/create.d.ts +1 -0
  16. package/dist/ddl/create.js +1 -1
  17. package/dist/ddl/diff.js +25 -23
  18. package/dist/designer/ColumnsEditor.js +8 -6
  19. package/dist/designer/ForeignKeysEditor.js +5 -1
  20. package/dist/designer/OptionsEditor.js +12 -3
  21. package/dist/designer/SqlPreview.d.ts +1 -5
  22. package/dist/designer/SqlPreview.js +1 -1
  23. package/dist/designer/TableDesigner.js +7 -6
  24. package/dist/explorer/DatabaseExplorer.js +12 -13
  25. package/dist/explorer/TableDialog.js +1 -1
  26. package/dist/explorer/tree.js +6 -16
  27. package/dist/grid/DataGrid.js +11 -14
  28. package/dist/grid/EnumPicker.js +4 -3
  29. package/dist/grid/GridHeaderCell.d.ts +1 -1
  30. package/dist/grid/GridHeaderCell.js +2 -2
  31. package/dist/grid/RecordView.js +2 -1
  32. package/dist/grid/ValueDock.js +2 -6
  33. package/dist/grid/aggregates.js +2 -3
  34. package/dist/grid/column-layout.js +3 -2
  35. package/dist/grid/copy-formats.js +2 -3
  36. package/dist/grid/display.d.ts +0 -1
  37. package/dist/grid/display.js +1 -3
  38. package/dist/grid/layout.js +6 -11
  39. package/dist/host/helper-path.js +8 -0
  40. package/dist/host/host.js +11 -16
  41. package/dist/host/validate.js +44 -43
  42. package/dist/sql.d.ts +2 -0
  43. package/dist/sql.js +5 -2
  44. package/dist/structure/StructureView.js +5 -6
  45. package/dist/table/TableView.js +19 -12
  46. package/dist/table/command-field.js +8 -12
  47. package/dist/table/grid-rows.js +2 -1
  48. package/dist/table/layout-store.js +5 -2
  49. package/dist/table/pending.js +11 -3
  50. package/dist/table/useTableTransfer.js +6 -5
  51. package/dist/testing/fake.js +7 -9
  52. package/dist/value/ValueEditor.js +1 -1
  53. package/dist/value/draft.js +1 -6
  54. package/dist/value/json.js +4 -4
  55. package/dist/workbench/DatabaseWorkbench.js +12 -5
  56. package/dist/workbench/tabs.js +3 -4
  57. package/package.json +6 -6
  58. package/src/assign-ref.ts +10 -0
  59. package/src/client/client.ts +17 -18
  60. package/src/client/types.ts +3 -0
  61. package/src/connections/ConnectionForm.tsx +62 -47
  62. package/src/connections/ConnectionManager.tsx +6 -5
  63. package/src/connections/EngineIcon.tsx +0 -2
  64. package/src/console/QueryConsole.tsx +52 -31
  65. package/src/console/editing.ts +2 -4
  66. package/src/console/useConsoleHistory.ts +5 -2
  67. package/src/ddl/alter-mysql.ts +3 -11
  68. package/src/ddl/alter-sqlite.ts +1 -1
  69. package/src/ddl/column.ts +5 -7
  70. package/src/ddl/create.ts +1 -1
  71. package/src/ddl/diff.ts +30 -25
  72. package/src/designer/ColumnsEditor.tsx +145 -125
  73. package/src/designer/ForeignKeysEditor.tsx +44 -27
  74. package/src/designer/OptionsEditor.tsx +95 -75
  75. package/src/designer/SqlPreview.tsx +2 -3
  76. package/src/designer/TableDesigner.tsx +7 -6
  77. package/src/explorer/DatabaseExplorer.tsx +26 -31
  78. package/src/explorer/TableDialog.tsx +1 -1
  79. package/src/explorer/tree.ts +8 -19
  80. package/src/grid/DataGrid.tsx +11 -14
  81. package/src/grid/EnumPicker.tsx +4 -3
  82. package/src/grid/GridHeaderCell.tsx +2 -3
  83. package/src/grid/RecordView.tsx +2 -1
  84. package/src/grid/ValueDock.tsx +2 -5
  85. package/src/grid/aggregates.ts +2 -4
  86. package/src/grid/column-layout.ts +3 -2
  87. package/src/grid/copy-formats.ts +2 -3
  88. package/src/grid/display.ts +1 -5
  89. package/src/grid/layout.ts +10 -11
  90. package/src/host/helper-path.ts +8 -8
  91. package/src/host/host.ts +11 -16
  92. package/src/host/validate.ts +45 -43
  93. package/src/protocol/messages.ts +0 -1
  94. package/src/sql.ts +7 -2
  95. package/src/structure/StructureView.tsx +7 -8
  96. package/src/table/CommandField.tsx +0 -1
  97. package/src/table/TableView.tsx +48 -34
  98. package/src/table/command-field.ts +9 -12
  99. package/src/table/grid-rows.ts +2 -7
  100. package/src/table/layout-store.ts +6 -3
  101. package/src/table/pending.ts +12 -7
  102. package/src/table/useTableTransfer.ts +7 -5
  103. package/src/testing/fake.ts +8 -12
  104. package/src/value/ValueEditor.tsx +1 -1
  105. package/src/value/draft.ts +1 -6
  106. package/src/value/json.ts +4 -4
  107. package/src/workbench/DatabaseWorkbench.tsx +14 -6
  108. package/src/workbench/DesignerTab.tsx +1 -1
  109. package/src/workbench/tabs.ts +5 -8
@@ -13,6 +13,7 @@ const fail = (message) => {
13
13
  throw new InvalidRequest(message);
14
14
  };
15
15
  const isDict = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
16
+ const isRequestId = (value) => typeof value === 'string' && value.length > 0 && value.length <= MAX_ID_LENGTH;
16
17
  /* Rejects a key nobody asked for, so a typo cannot pass for an option the helper ignores. */
17
18
  const shape = (value, label, required, optional = []) => {
18
19
  if (!isDict(value)) {
@@ -30,12 +31,12 @@ const shape = (value, label, required, optional = []) => {
30
31
  }
31
32
  return value;
32
33
  };
33
- const text = (value, label) => {
34
+ const filledText = (value, label) => {
34
35
  if (typeof value !== 'string' || value.length === 0) {
35
36
  fail(`"${label}" must be a non-empty string.`);
36
37
  }
37
38
  };
38
- const plainText = (value, label) => {
39
+ const anyText = (value, label) => {
39
40
  if (typeof value !== 'string') {
40
41
  fail(`"${label}" must be a string.`);
41
42
  }
@@ -56,7 +57,7 @@ const oneOf = (allowed) => (value, label) => {
56
57
  }
57
58
  };
58
59
  const absolutePath = (value, label) => {
59
- text(value, label);
60
+ filledText(value, label);
60
61
  if (!ABSOLUTE_PATH.test(value)) {
61
62
  fail(`"${label}" must be an absolute path.`);
62
63
  }
@@ -112,17 +113,17 @@ const tunnel = (input, label) => {
112
113
  }
113
114
  if (input.kind === 'ssh') {
114
115
  const config = shape(input, label, ['kind', 'host'], ['port', 'user', 'identityFile']);
115
- text(config.host, `${label}.host`);
116
+ filledText(config.host, `${label}.host`);
116
117
  optional(config, 'port', label, port);
117
- optional(config, 'user', label, text);
118
- optional(config, 'identityFile', label, text);
118
+ optional(config, 'user', label, filledText);
119
+ optional(config, 'identityFile', label, filledText);
119
120
  return;
120
121
  }
121
122
  if (input.kind === 'docker') {
122
123
  const config = shape(input, label, ['kind', 'container'], ['port', 'context']);
123
- text(config.container, `${label}.container`);
124
+ filledText(config.container, `${label}.container`);
124
125
  optional(config, 'port', label, port);
125
- optional(config, 'context', label, text);
126
+ optional(config, 'context', label, filledText);
126
127
  return;
127
128
  }
128
129
  fail(`"${label}.kind" must be "ssh" or "docker".`);
@@ -144,12 +145,12 @@ const connection = (input, label) => {
144
145
  optional(config, 'tunnel', label, tunnel);
145
146
  // A Docker tunnel finds the server by its container, so the host is left empty.
146
147
  const viaDocker = isDict(config.tunnel) && config.tunnel.kind === 'docker';
147
- (viaDocker ? plainText : text)(config.host, `${label}.host`);
148
- text(config.user, `${label}.user`);
148
+ (viaDocker ? anyText : filledText)(config.host, `${label}.host`);
149
+ filledText(config.user, `${label}.user`);
149
150
  optional(config, 'port', label, port);
150
- optional(config, 'socket', label, text);
151
- optional(config, 'password', label, plainText);
152
- optional(config, 'database', label, plainText);
151
+ optional(config, 'socket', label, filledText);
152
+ optional(config, 'password', label, anyText);
153
+ optional(config, 'database', label, anyText);
153
154
  optional(config, 'tls', label, oneOf(TLS_MODES));
154
155
  optional(config, 'readOnly', label, flag);
155
156
  return;
@@ -185,43 +186,43 @@ const exportSource = (input, label) => {
185
186
  }
186
187
  if (input.kind === 'table') {
187
188
  const source = shape(input, label, ['kind', 'schema', 'table'], ['where', 'orderBy']);
188
- text(source.schema, `${label}.schema`);
189
- text(source.table, `${label}.table`);
190
- optional(source, 'where', label, plainText);
191
- optional(source, 'orderBy', label, plainText);
189
+ filledText(source.schema, `${label}.schema`);
190
+ filledText(source.table, `${label}.table`);
191
+ optional(source, 'where', label, anyText);
192
+ optional(source, 'orderBy', label, anyText);
192
193
  return;
193
194
  }
194
195
  if (input.kind === 'query') {
195
196
  const source = shape(input, label, ['kind', 'sql'], ['schema']);
196
- text(source.sql, `${label}.sql`);
197
- optional(source, 'schema', label, text);
197
+ filledText(source.sql, `${label}.sql`);
198
+ optional(source, 'schema', label, filledText);
198
199
  return;
199
200
  }
200
201
  fail(`"${label}.kind" must be "table" or "query".`);
201
202
  };
202
203
  const cellLimit = (params) => optional(params, 'cellLimit', 'params', (limit, name) => integer(limit, name, 1, MAX_CELL_LIMIT));
203
204
  const table = (params) => {
204
- text(params.session, 'params.session');
205
- text(params.schema, 'params.schema');
206
- text(params.table, 'params.table');
205
+ filledText(params.session, 'params.session');
206
+ filledText(params.schema, 'params.schema');
207
+ filledText(params.table, 'params.table');
207
208
  };
208
209
  const TARGET = ['session', 'schema', 'table'];
209
210
  const PARAMS = {
210
211
  open: (input) => connection(shape(input, 'params', ['connection']).connection, 'params.connection'),
211
212
  test: (input) => connection(shape(input, 'params', ['connection']).connection, 'params.connection'),
212
- close: (input) => text(shape(input, 'params', ['session']).session, 'params.session'),
213
- schemas: (input) => text(shape(input, 'params', ['session']).session, 'params.session'),
213
+ close: (input) => filledText(shape(input, 'params', ['session']).session, 'params.session'),
214
+ schemas: (input) => filledText(shape(input, 'params', ['session']).session, 'params.session'),
214
215
  tables: (input) => {
215
216
  const params = shape(input, 'params', ['session', 'schema']);
216
- text(params.session, 'params.session');
217
- text(params.schema, 'params.schema');
217
+ filledText(params.session, 'params.session');
218
+ filledText(params.schema, 'params.schema');
218
219
  },
219
220
  structure: (input) => table(shape(input, 'params', TARGET)),
220
221
  rows: (input) => {
221
222
  const params = shape(input, 'params', [...TARGET, 'offset', 'limit'], ['where', 'orderBy', 'cellLimit']);
222
223
  table(params);
223
- optional(params, 'where', 'params', plainText);
224
- optional(params, 'orderBy', 'params', plainText);
224
+ optional(params, 'where', 'params', anyText);
225
+ optional(params, 'orderBy', 'params', anyText);
225
226
  integer(params.offset, 'params.offset', 0, Number.MAX_SAFE_INTEGER);
226
227
  integer(params.limit, 'params.limit', 1, MAX_LIMIT);
227
228
  cellLimit(params);
@@ -229,13 +230,13 @@ const PARAMS = {
229
230
  count: (input) => {
230
231
  const params = shape(input, 'params', TARGET, ['where']);
231
232
  table(params);
232
- optional(params, 'where', 'params', plainText);
233
+ optional(params, 'where', 'params', anyText);
233
234
  },
234
235
  cell: (input) => {
235
236
  const params = shape(input, 'params', [...TARGET, 'key', 'column']);
236
237
  table(params);
237
238
  values(params.key, 'params.key', value, true);
238
- text(params.column, 'params.column');
239
+ filledText(params.column, 'params.column');
239
240
  },
240
241
  apply: (input) => {
241
242
  const params = shape(input, 'params', [...TARGET, 'changes']);
@@ -248,34 +249,34 @@ const PARAMS = {
248
249
  },
249
250
  execute: (input) => {
250
251
  const params = shape(input, 'params', ['session', 'sql'], ['schema', 'limit', 'cellLimit']);
251
- text(params.session, 'params.session');
252
- text(params.sql, 'params.sql');
253
- optional(params, 'schema', 'params', text);
252
+ filledText(params.session, 'params.session');
253
+ filledText(params.sql, 'params.sql');
254
+ optional(params, 'schema', 'params', filledText);
254
255
  optional(params, 'limit', 'params', (limit, name) => integer(limit, name, 1, MAX_LIMIT));
255
256
  cellLimit(params);
256
257
  },
257
258
  page: (input) => {
258
259
  const params = shape(input, 'params', ['session', 'sql', 'offset', 'limit'], ['schema', 'cellLimit']);
259
- text(params.session, 'params.session');
260
- text(params.sql, 'params.sql');
261
- optional(params, 'schema', 'params', text);
260
+ filledText(params.session, 'params.session');
261
+ filledText(params.sql, 'params.sql');
262
+ optional(params, 'schema', 'params', filledText);
262
263
  integer(params.offset, 'params.offset', 0, Number.MAX_SAFE_INTEGER);
263
264
  integer(params.limit, 'params.limit', 1, MAX_LIMIT);
264
265
  cellLimit(params);
265
266
  },
266
267
  transaction: (input) => {
267
268
  const params = shape(input, 'params', ['session', 'action']);
268
- text(params.session, 'params.session');
269
+ filledText(params.session, 'params.session');
269
270
  oneOf(TRANSACTION_ACTIONS)(params.action, 'params.action');
270
271
  },
271
272
  export: (input) => {
272
273
  const params = shape(input, 'params', ['session', 'source', 'format', 'path'], ['header', 'tableName']);
273
- text(params.session, 'params.session');
274
+ filledText(params.session, 'params.session');
274
275
  exportSource(params.source, 'params.source');
275
276
  oneOf(EXPORT_FORMATS)(params.format, 'params.format');
276
277
  absolutePath(params.path, 'params.path');
277
278
  optional(params, 'header', 'params', flag);
278
- optional(params, 'tableName', 'params', text);
279
+ optional(params, 'tableName', 'params', filledText);
279
280
  },
280
281
  sample: (input) => {
281
282
  const params = shape(input, 'params', ['path', 'format', 'header'], ['limit']);
@@ -296,25 +297,25 @@ const PARAMS = {
296
297
  }
297
298
  params.columns.forEach((column, i) => {
298
299
  if (column !== null) {
299
- text(column, `params.columns[${i}]`);
300
+ filledText(column, `params.columns[${i}]`);
300
301
  }
301
302
  });
302
303
  },
303
304
  discover: (input) => {
304
305
  const params = shape(input, 'params', ['kind'], ['context']);
305
306
  oneOf(['docker'])(params.kind, 'params.kind');
306
- optional(params, 'context', 'params', text);
307
+ optional(params, 'context', 'params', filledText);
307
308
  },
308
309
  cancel: (input) => {
309
310
  const params = shape(input, 'params', ['request']);
310
- if (typeof params.request !== 'string' || params.request.length === 0 || params.request.length > MAX_ID_LENGTH) {
311
+ if (!isRequestId(params.request)) {
311
312
  fail(`"params.request" must be a string of 1 to ${MAX_ID_LENGTH} characters.`);
312
313
  }
313
314
  }
314
315
  };
315
316
  /* Checks the whole request before anything reaches the helper, which then never has to defend itself against a page. */
316
317
  export const parseRequest = (input) => {
317
- const id = isDict(input) && typeof input.id === 'string' && input.id.length > 0 && input.id.length <= MAX_ID_LENGTH ? input.id : '';
318
+ const id = isDict(input) && isRequestId(input.id) ? input.id : '';
318
319
  try {
319
320
  const envelope = shape(input, 'request', ['id', 'method', 'params']);
320
321
  if (id === '') {
package/dist/sql.d.ts CHANGED
@@ -12,6 +12,8 @@ export interface SqlTarget {
12
12
  export type LiteralValue = Cell | Value | {
13
13
  readonly kind: 'default';
14
14
  };
15
+ export declare const NUMERIC_TEXT: RegExp;
16
+ export declare const isNumericKind: (kind: ValueKind) => boolean;
15
17
  export declare const quoteIdentifier: (engine: Engine, name: string) => string;
16
18
  export declare const qualifiedName: (target: SqlTarget) => string;
17
19
  export declare const sqlLiteral: (engine: Engine, value: LiteralValue, kind?: ValueKind) => string;
package/dist/sql.js CHANGED
@@ -1,5 +1,8 @@
1
1
  const NUMERIC_KINDS = new Set(['integer', 'decimal', 'float']);
2
- const NUMERIC_TEXT = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
2
+ /* A number as SQL and most people write it: a sign, digits with an optional fraction, and an exponent. */
3
+ export const NUMERIC_TEXT = /^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/;
4
+ /* Whether a value of this kind is a number, which a grid aligns against the end of its cell. */
5
+ export const isNumericKind = (kind) => NUMERIC_KINDS.has(kind);
3
6
  const ORDER_ITEM = /\s*(`(?:[^`]|``)+`|"(?:[^"]|"")+"|[A-Za-z_][\w$]*)(?:\s+(asc|desc))?\s*(,|$)/iy;
4
7
  const markOf = (engine) => (engine === 'mysql' ? '`' : '"');
5
8
  export const quoteIdentifier = (engine, name) => {
@@ -28,7 +31,7 @@ export const sqlLiteral = (engine, value, kind) => {
28
31
  return Number.isFinite(value) ? String(value) : 'NULL';
29
32
  case 'string': {
30
33
  const trimmed = value.trim();
31
- return kind !== undefined && NUMERIC_KINDS.has(kind) && NUMERIC_TEXT.test(trimmed) ? trimmed : stringLiteral(engine, value);
34
+ return kind !== undefined && isNumericKind(kind) && NUMERIC_TEXT.test(trimmed) ? trimmed : stringLiteral(engine, value);
32
35
  }
33
36
  }
34
37
  switch (value.kind) {
@@ -10,14 +10,13 @@ import { useSchemaChange } from '../use-schema-change.js';
10
10
  import { referenceOf } from './structure-text.js';
11
11
  const TH = 'sticky top-0 h-7 bg-surface px-3 text-left text-xs font-medium whitespace-nowrap text-text-faint';
12
12
  const TD = 'h-7 border-t border-border-soft px-3 text-xs whitespace-nowrap text-text select-text';
13
- const MONO = CODE_TEXT;
14
13
  /* The columns, indexes, foreign keys and DDL of one table or view. */
15
14
  export function StructureView({ connection, schema, table, toolbarStart, className, ref }) {
16
15
  const { t } = useTranslation('database');
17
16
  const client = useDatabaseClient();
18
17
  const [answer, setAnswer] = useState(null);
19
18
  const [attempt, setAttempt] = useState(0);
20
- // A schema change reloads without leaving the structure on screen: the answer keeps its place until the new one arrives.
19
+ // A schema change reloads in place: the old answer stays on screen until the new one arrives.
21
20
  const [reloads, setReloads] = useState(0);
22
21
  const answered = answer !== null && answer.connection === connection && answer.schema === schema && answer.table === table && answer.attempt === attempt;
23
22
  const load = answered ? answer.load : { status: 'loading' };
@@ -69,14 +68,14 @@ function ColumnsTable({ structure }) {
69
68
  }
70
69
  function ColumnRow({ column, primary, foreign }) {
71
70
  const { t } = useTranslation('database');
72
- return (_jsxs("tr", { children: [_jsx("td", { className: TD, children: _jsxs("span", { className: "flex items-center gap-1.5", children: [primary ? (_jsx(Icon, { icon: KeyRound, size: 12, className: "shrink-0 text-status-needs-you" })) : foreign ? (_jsx(Icon, { icon: Link2, size: 12, className: "shrink-0 text-text-muted" })) : (_jsx("span", { className: "size-3 shrink-0" })), column.name] }) }), _jsx("td", { className: clsx(TD, MONO), children: column.type }), _jsx("td", { className: clsx(TD, 'text-text-muted'), children: _jsx(YesNo, { value: column.nullable }) }), _jsx("td", { className: clsx(TD, MONO), children: column.defaultValue }), _jsx("td", { className: TD, children: _jsxs("span", { className: "flex gap-1", children: [column.autoIncrement && _jsx(Pill, { children: t('structure.autoIncrement') }), column.generated && _jsx(Pill, { children: t('structure.generated') })] }) }), _jsx("td", { className: clsx(TD, 'text-text-muted'), children: column.comment })] }));
71
+ return (_jsxs("tr", { children: [_jsx("td", { className: TD, children: _jsxs("span", { className: "flex items-center gap-1.5", children: [primary ? (_jsx(Icon, { icon: KeyRound, size: 12, className: "shrink-0 text-status-needs-you" })) : foreign ? (_jsx(Icon, { icon: Link2, size: 12, className: "shrink-0 text-text-muted" })) : (_jsx("span", { className: "size-3 shrink-0" })), column.name] }) }), _jsx("td", { className: clsx(TD, CODE_TEXT), children: column.type }), _jsx("td", { className: clsx(TD, 'text-text-muted'), children: _jsx(YesNo, { value: column.nullable }) }), _jsx("td", { className: clsx(TD, CODE_TEXT), children: column.defaultValue }), _jsx("td", { className: TD, children: _jsxs("span", { className: "flex gap-1", children: [column.autoIncrement && _jsx(Pill, { children: t('structure.autoIncrement') }), column.generated && _jsx(Pill, { children: t('structure.generated') })] }) }), _jsx("td", { className: clsx(TD, 'text-text-muted'), children: column.comment })] }));
73
72
  }
74
73
  function IndexesTable({ indexes }) {
75
74
  const { t } = useTranslation('database');
76
75
  if (indexes.length === 0) {
77
76
  return _jsx(EmptyState, { children: t('structure.noIndexes') });
78
77
  }
79
- return (_jsx(Grid, { heads: [t('structure.index.name'), t('structure.index.columns'), t('structure.index.kind')], children: indexes.map((index) => (_jsxs("tr", { children: [_jsx("td", { className: TD, children: index.name }), _jsx("td", { className: clsx(TD, MONO), children: index.columns.join(', ') }), _jsx("td", { className: TD, children: _jsxs("span", { className: "flex gap-1", children: [index.primary && _jsx(Pill, { tone: "accent", children: t('structure.primary') }), index.unique && !index.primary && _jsx(Pill, { children: t('structure.unique') })] }) })] }, index.name))) }));
78
+ return (_jsx(Grid, { heads: [t('structure.index.name'), t('structure.index.columns'), t('structure.index.kind')], children: indexes.map((index) => (_jsxs("tr", { children: [_jsx("td", { className: TD, children: index.name }), _jsx("td", { className: clsx(TD, CODE_TEXT), children: index.columns.join(', ') }), _jsx("td", { className: TD, children: _jsxs("span", { className: "flex gap-1", children: [index.primary && _jsx(Pill, { tone: "accent", children: t('structure.primary') }), index.unique && !index.primary && _jsx(Pill, { children: t('structure.unique') })] }) })] }, index.name))) }));
80
79
  }
81
80
  function ForeignKeysTable({ foreignKeys }) {
82
81
  const { t } = useTranslation('database');
@@ -90,12 +89,12 @@ function ForeignKeysTable({ foreignKeys }) {
90
89
  t('structure.foreignKey.references'),
91
90
  t('structure.foreignKey.onUpdate'),
92
91
  t('structure.foreignKey.onDelete')
93
- ], children: foreignKeys.map((key, i) => (_jsxs("tr", { children: [_jsx("td", { className: TD, children: key.name }), _jsx("td", { className: clsx(TD, MONO), children: key.columns.join(', ') }), _jsx("td", { className: clsx(TD, MONO), children: referenceOf(key) }), _jsx("td", { className: clsx(TD, 'text-text-muted'), children: action(key.onUpdate) }), _jsx("td", { className: clsx(TD, 'text-text-muted'), children: action(key.onDelete) })] }, key.name ?? i))) }));
92
+ ], children: foreignKeys.map((key, i) => (_jsxs("tr", { children: [_jsx("td", { className: TD, children: key.name }), _jsx("td", { className: clsx(TD, CODE_TEXT), children: key.columns.join(', ') }), _jsx("td", { className: clsx(TD, CODE_TEXT), children: referenceOf(key) }), _jsx("td", { className: clsx(TD, 'text-text-muted'), children: action(key.onUpdate) }), _jsx("td", { className: clsx(TD, 'text-text-muted'), children: action(key.onDelete) })] }, key.name ?? i))) }));
94
93
  }
95
94
  function Ddl({ ddl }) {
96
95
  const { t } = useTranslation('database');
97
96
  if (ddl === null) {
98
97
  return _jsx(EmptyState, { children: t('structure.noDdl') });
99
98
  }
100
- return (_jsxs("div", { className: "relative", children: [_jsxs(Button, { variant: "secondary", size: "xs", className: "absolute top-2 right-3", onClick: () => copyText(ddl), children: [_jsx(Icon, { icon: Copy, size: 12 }), t('structure.copy')] }), _jsx("pre", { className: clsx(MONO, 'overflow-auto p-3 pr-24 whitespace-pre text-text select-text'), children: ddl })] }));
99
+ return (_jsxs("div", { className: "relative", children: [_jsxs(Button, { variant: "secondary", size: "xs", className: "absolute top-2 right-3", onClick: () => copyText(ddl), children: [_jsx(Icon, { icon: Copy, size: 12 }), t('structure.copy')] }), _jsx("pre", { className: clsx(CODE_TEXT, 'overflow-auto p-3 pr-24 whitespace-pre text-text select-text'), children: ddl })] }));
101
100
  }
@@ -40,6 +40,18 @@ const parseRecordWidth = (raw) => {
40
40
  }
41
41
  };
42
42
  const NO_KEYS = new Set();
43
+ const readOnlyReasonOf = (connection, structure, t) => {
44
+ if (connection.config.readOnly === true) {
45
+ return t('table.readOnly.connection');
46
+ }
47
+ if (structure?.kind === 'view') {
48
+ return t('table.readOnly.view');
49
+ }
50
+ if (structure !== null && structure.rowKey === null) {
51
+ return t('table.readOnly.noKey');
52
+ }
53
+ return null;
54
+ };
43
55
  /* What the strip over the table, or the app's notice, says of an export or an import. */
44
56
  const transferText = (found, t) => {
45
57
  switch (found.kind) {
@@ -53,7 +65,6 @@ const transferText = (found, t) => {
53
65
  return found.message;
54
66
  }
55
67
  };
56
- /* How much of a value a menu item spells out before it cuts it. */
57
68
  const MENU_VALUE_LENGTH = 40;
58
69
  const FOCUS_COMMAND = shortcut('Mod+F');
59
70
  const DUPLICATE_ROWS = shortcut('Mod+D');
@@ -112,15 +123,7 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
112
123
  const structure = structureLoad.value;
113
124
  const loaded = rowsLoad.value?.result ?? null;
114
125
  const loadedOffset = rowsLoad.value?.offset ?? 0;
115
- const readOnlyReason = connection.config.readOnly === true
116
- ? t('table.readOnly.connection')
117
- : structure === null
118
- ? null
119
- : structure.kind === 'view'
120
- ? t('table.readOnly.view')
121
- : structure.rowKey === null
122
- ? t('table.readOnly.noKey')
123
- : null;
126
+ const readOnlyReason = readOnlyReasonOf(connection, structure, t);
124
127
  const editable = readOnlyReason === null && structure !== null && loaded !== null && !rowsLoad.loading;
125
128
  const columns = useMemo(() => (loaded === null ? [] : buildGridColumns(loaded, structure)), [loaded, structure]);
126
129
  const rows = useMemo(() => (loaded === null ? [] : buildGridRows({ structure, loaded, pending, offset: loadedOffset })), [loaded, structure, pending, loadedOffset]);
@@ -220,7 +223,7 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
220
223
  });
221
224
  }
222
225
  };
223
- /* Every change of the chips applies at once; one that changes the page asks first when it would throw pending changes away. */
226
+ /* A change that leaves the SQL alone only remembers the filters and keeps the page. */
224
227
  const applyChips = (next) => {
225
228
  const where = chipsToWhere(next);
226
229
  const orderBy = chipsToOrderBy(engine, next);
@@ -469,7 +472,7 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
469
472
  busy: transfer.notice?.kind === 'exporting',
470
473
  onExport: transfer.exportAs,
471
474
  onImport: readOnlyReason === null && structure !== null ? () => guard(transfer.chooseImportFile) : undefined
472
- }, pendingCount: changeCount, submitting: submitting, engine: engine, chips: chips, columns: commandColumns, pageSize: pageSize, bounds: bounds, commandRef: command, onChipsChange: applyChips, onJumpToColumn: jumpToColumn, onRefresh: refresh, onAddRow: () => setPending((now) => addInsertWith(now, {})), onDeleteRows: deleteSelected, onCloneRows: () => cloneRows([...selected]), onRevertRows: () => revertRowKeys(selected), onToggleRecordView: toggleRecordView, onPageSizeChange: changePageSize, onFirstPage: () => goToPage(0), onLastPage: () => void goToLast(), onSubmit: () => void submit(), onRevert: discardPending }), notice !== null && (_jsx(Banner, { icon: CircleAlert, tone: "error", message: notice, className: "shrink-0 pt-2", children: failure === null ? (_jsx(Button, { size: "xs", onClick: refresh, children: t('table.retry') })) : (_jsx(Button, { size: "xs", onClick: () => setFailure(null), children: t('table.dismiss') })) })), shapeChanged && (_jsx(Banner, { icon: RefreshCw, tone: "neutral", message: t('table.changed'), className: "shrink-0 pt-2", children: _jsx(Button, { size: "xs", onClick: refresh, children: t('table.reload') }) })), transfer.notice !== null && (_jsx(Banner, { icon: transfer.notice.kind === 'failed' ? CircleAlert : transfer.notice.kind === 'exporting' ? Download : Check, tone: transfer.notice.kind === 'failed' ? 'error' : 'neutral', message: transferText(transfer.notice, t), className: "shrink-0 pt-2", children: transfer.notice.kind === 'exporting' ? (_jsxs(_Fragment, { children: [_jsx(Spinner, { size: 12, label: t('table.export.running', { format: transfer.notice.format.toUpperCase() }) }), _jsx(Button, { size: "xs", onClick: transfer.cancelExport, children: t('table.export.cancel') })] })) : (_jsx(Button, { size: "xs", onClick: transfer.dismiss, children: t('table.dismiss') })) })), loaded === null ? (_jsx(PanelEmpty, { busy: notice === null, children: notice === null ? t('table.loading') : t('table.notLoaded') })) : (_jsxs(_Fragment, { children: [_jsx(ValueDock, { open: recordOpen, width: recordWidth ?? RECORD_WIDTH, onWidthChange: setRecordWidth, panel: _jsx(RecordView, { label: t('table.recordFields', { table }), columns: columns, rows: rows, index: recordIndex, onIndexChange: changeRecordIndex, focusedColumn: focus !== null && focusedIndex === recordIndex ? focus.column : null, editable: editable, onCommit: commit, loadValue: loadValue, onFocusedCellChange: followInGrid, onFollow: onAction === undefined ? undefined : follow, canFollow: (cell) => referenceAt(cell) !== null, onClose: () => setRecordOpen(false), empty: t('table.empty') }), children: _jsx("div", { "aria-busy": rowsLoad.loading, className: clsx('relative flex min-h-0 flex-1 flex-col', rowsLoad.loading && 'pointer-events-none opacity-60'), children: _jsx(DataGrid, { ref: grid, label: t('table.grid', { table }), columns: columns, rows: rows, editable: editable, empty: t('table.empty'), selectedKeys: selected, onSelectedKeysChange: setSelected, onCommit: commit, loadValue: loadValue, sorts: sorts, onSortsChange: changeSorts, onFocusedCellChange: setFocus, onDeleteSelected: editable ? deleteSelected : undefined, sqlTarget: sqlTarget, queryMenu: queryMenu, menu: gridMenu, onDuplicateRows: editable ? cloneRows : undefined, onFollow: onAction === undefined ? undefined : follow, canFollow: (cell) => referenceAt(cell) !== null, onRangeChange: setRange, initialLayout: stored ?? undefined, onLayoutChange: remember, focusRequest: focusRequest, columnRequest: columnRequest }) }) }), _jsx(TableStatusBar, { elapsedMs: loaded.elapsedMs, bounds: bounds, counting: counting, selection: selectionFigures, onCount: () => void countRows(), onPrevious: () => goToPage(offset - pageSize), onNext: () => goToPage(offset + pageSize) })] })), transfer.importDraft !== null && structure !== null && (_jsx(ImportDialog, { open: true, path: transfer.importDraft.path, tableColumns: importableColumns(structure.columns), sample: transfer.importDraft.sample, header: transfer.importDraft.header, mapping: transfer.importDraft.mapping, busy: transfer.importDraft.busy, error: transfer.importDraft.error, onHeaderChange: transfer.changeHeader, onMappingChange: transfer.changeMapping, onImport: transfer.runImport, onCancel: transfer.closeImport, onOpenChange: (open) => !open && transfer.closeImport() })), _jsx(PromptDialog, { open: discard !== null, danger: true, title: t('table.discard.title'), description: t('table.discard.description'), confirmLabel: t('table.discard.confirm'), onConfirm: () => {
475
+ }, pendingCount: changeCount, submitting: submitting, engine: engine, chips: chips, columns: commandColumns, pageSize: pageSize, bounds: bounds, commandRef: command, onChipsChange: applyChips, onJumpToColumn: jumpToColumn, onRefresh: refresh, onAddRow: () => setPending((now) => addInsertWith(now, {})), onDeleteRows: deleteSelected, onCloneRows: () => cloneRows([...selected]), onRevertRows: () => revertRowKeys(selected), onToggleRecordView: toggleRecordView, onPageSizeChange: changePageSize, onFirstPage: () => goToPage(0), onLastPage: () => void goToLast(), onSubmit: () => void submit(), onRevert: discardPending }), notice !== null && (_jsx(Banner, { icon: CircleAlert, tone: "error", message: notice, className: "shrink-0 pt-2", children: failure === null ? (_jsx(Button, { size: "xs", onClick: refresh, children: t('table.retry') })) : (_jsx(Button, { size: "xs", onClick: () => setFailure(null), children: t('table.dismiss') })) })), shapeChanged && (_jsx(Banner, { icon: RefreshCw, tone: "neutral", message: t('table.changed'), className: "shrink-0 pt-2", children: _jsx(Button, { size: "xs", onClick: refresh, children: t('table.reload') }) })), transfer.notice !== null && _jsx(TransferBanner, { notice: transfer.notice, onCancel: transfer.cancelExport, onDismiss: transfer.dismiss }), loaded === null ? (_jsx(PanelEmpty, { busy: notice === null, children: notice === null ? t('table.loading') : t('table.notLoaded') })) : (_jsxs(_Fragment, { children: [_jsx(ValueDock, { open: recordOpen, width: recordWidth ?? RECORD_WIDTH, onWidthChange: setRecordWidth, panel: _jsx(RecordView, { label: t('table.recordFields', { table }), columns: columns, rows: rows, index: recordIndex, onIndexChange: changeRecordIndex, focusedColumn: focus !== null && focusedIndex === recordIndex ? focus.column : null, editable: editable, onCommit: commit, loadValue: loadValue, onFocusedCellChange: followInGrid, onFollow: onAction === undefined ? undefined : follow, canFollow: (cell) => referenceAt(cell) !== null, onClose: () => setRecordOpen(false), empty: t('table.empty') }), children: _jsx("div", { "aria-busy": rowsLoad.loading, className: clsx('relative flex min-h-0 flex-1 flex-col', rowsLoad.loading && 'pointer-events-none opacity-60'), children: _jsx(DataGrid, { ref: grid, label: t('table.grid', { table }), columns: columns, rows: rows, editable: editable, empty: t('table.empty'), selectedKeys: selected, onSelectedKeysChange: setSelected, onCommit: commit, loadValue: loadValue, sorts: sorts, onSortsChange: changeSorts, onFocusedCellChange: setFocus, onDeleteSelected: editable ? deleteSelected : undefined, sqlTarget: sqlTarget, queryMenu: queryMenu, menu: gridMenu, onDuplicateRows: editable ? cloneRows : undefined, onFollow: onAction === undefined ? undefined : follow, canFollow: (cell) => referenceAt(cell) !== null, onRangeChange: setRange, initialLayout: stored ?? undefined, onLayoutChange: remember, focusRequest: focusRequest, columnRequest: columnRequest }) }) }), _jsx(TableStatusBar, { elapsedMs: loaded.elapsedMs, bounds: bounds, counting: counting, selection: selectionFigures, onCount: () => void countRows(), onPrevious: () => goToPage(offset - pageSize), onNext: () => goToPage(offset + pageSize) })] })), transfer.importDraft !== null && structure !== null && (_jsx(ImportDialog, { open: true, path: transfer.importDraft.path, tableColumns: importableColumns(structure.columns), sample: transfer.importDraft.sample, header: transfer.importDraft.header, mapping: transfer.importDraft.mapping, busy: transfer.importDraft.busy, error: transfer.importDraft.error, onHeaderChange: transfer.changeHeader, onMappingChange: transfer.changeMapping, onImport: transfer.runImport, onCancel: transfer.closeImport, onOpenChange: (open) => !open && transfer.closeImport() })), _jsx(PromptDialog, { open: discard !== null, danger: true, title: t('table.discard.title'), description: t('table.discard.description'), confirmLabel: t('table.discard.confirm'), onConfirm: () => {
473
476
  discard?.run();
474
477
  setDiscard(null);
475
478
  }, onOpenChange: (open) => {
@@ -478,3 +481,7 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
478
481
  }
479
482
  } })] }));
480
483
  }
484
+ function TransferBanner({ notice, onCancel, onDismiss }) {
485
+ const { t } = useTranslation('database');
486
+ return (_jsx(Banner, { icon: notice.kind === 'failed' ? CircleAlert : notice.kind === 'exporting' ? Download : Check, tone: notice.kind === 'failed' ? 'error' : 'neutral', message: transferText(notice, t), className: "shrink-0 pt-2", children: notice.kind === 'exporting' ? (_jsxs(_Fragment, { children: [_jsx(Spinner, { size: 12, label: t('table.export.running', { format: notice.format.toUpperCase() }) }), _jsx(Button, { size: "xs", onClick: onCancel, children: t('table.export.cancel') })] })) : (_jsx(Button, { size: "xs", onClick: onDismiss, children: t('table.dismiss') })) }));
487
+ }
@@ -14,6 +14,7 @@ const NAME_BEING_TYPED = /^\s*(?:[\w$]*|[`"][^`"]*[`"]?)$/;
14
14
  export const chipKey = (chip) => (chip.kind === 'sort' ? `sort:${chip.column}` : `${chip.kind}:${chip.kind === 'filter' ? chip.sql : chip.text}`);
15
15
  const isFilter = (chip) => chip.kind === 'filter';
16
16
  const isSort = (chip) => chip.kind === 'sort';
17
+ const sortChip = (sort) => ({ kind: 'sort', column: sort.column, direction: sort.direction });
17
18
  /* A name as typed in SQL: bare when it can be, else in double quotes. The quotes are only for the text of a chip; `sql` quotes per engine. */
18
19
  export const identifierText = (name) => (BARE_NAME.test(name) ? name : `"${name.replaceAll('"', '""')}"`);
19
20
  /* The conditions AND-ed together, each in parentheses when it holds an OR and there is more than one. */
@@ -39,10 +40,7 @@ export const chipsToOrderBy = (engine, chips) => chips
39
40
  .join(', ');
40
41
  export const sortsOf = (chips) => chips.filter(isSort).map(({ column, direction }) => ({ column, direction }));
41
42
  /* The sorts of a header menu replace every sort and raw ORDER BY, keeping the filters where they are. */
42
- export const withSorts = (chips, sorts) => [
43
- ...chips.filter(isFilter),
44
- ...sorts.map((sort) => ({ kind: 'sort', column: sort.column, direction: sort.direction }))
45
- ];
43
+ export const withSorts = (chips, sorts) => [...chips.filter(isFilter), ...sorts.map(sortChip)];
46
44
  /* A column that is not sorted joins as ascending; one that is flips its direction where it stands. */
47
45
  export const toggleSort = (chips, column) => {
48
46
  if (!chips.some((chip) => isSort(chip) && chip.column === column)) {
@@ -75,7 +73,7 @@ export const sortChipsOf = (engine, orderBy) => {
75
73
  return [];
76
74
  }
77
75
  const sorts = parseOrderByLoose(engine, text);
78
- return sorts === null ? [{ kind: 'order', text }] : sorts.map((sort) => ({ kind: 'sort', column: sort.column, direction: sort.direction }));
76
+ return sorts === null ? [{ kind: 'order', text }] : sorts.map(sortChip);
79
77
  };
80
78
  export const chipsFromQuery = (engine, { where, orderBy, remembered }) => [
81
79
  ...filterChipsOf(where, remembered),
@@ -112,13 +110,11 @@ const readOperation = (rest) => {
112
110
  const operator = compare[1] === '==' ? '=' : compare[1] === '!=' ? '<>' : compare[1];
113
111
  return isClosed(compare[2]) ? `${operator} ${compare[2]}` : null;
114
112
  }
115
- const pattern = PATTERN.exec(rest);
116
- if (pattern !== null) {
117
- return isClosed(pattern[2]) ? `${upper(pattern[1])} ${pattern[2]}` : null;
118
- }
119
- const member = MEMBER.exec(rest);
120
- if (member !== null) {
121
- return isClosed(member[2]) ? `${upper(member[1])} ${member[2]}` : null;
113
+ for (const form of [PATTERN, MEMBER]) {
114
+ const found = form.exec(rest);
115
+ if (found !== null) {
116
+ return isClosed(found[2]) ? `${upper(found[1])} ${found[2]}` : null;
117
+ }
122
118
  }
123
119
  const between = BETWEEN.exec(rest);
124
120
  if (between !== null) {
@@ -7,7 +7,8 @@ export const insertedRowKey = (id) => `new:${id}`;
7
7
  export const parseRowKey = (key) => key.startsWith('new:') ? { kind: 'inserted', id: Number(key.slice(4)) } : { kind: 'loaded', index: Number(key.slice(4)) };
8
8
  export const buildGridColumns = (loaded, structure) => {
9
9
  const keys = { primaryKey: structure?.primaryKey ?? [], foreignKey: structure?.foreignKeys.flatMap((key) => key.columns) ?? [] };
10
- return loaded.columns.map((column) => gridColumnOf(column, structure?.columns.find((info) => info.name === column.name), keys));
10
+ const infoOf = (name) => structure?.columns.find((info) => info.name === name);
11
+ return loaded.columns.map((column) => gridColumnOf(column, infoOf(column.name), keys));
11
12
  };
12
13
  /* The loaded rows with their pending edits laid over them, then the rows waiting to be inserted. */
13
14
  export const buildGridRows = ({ structure, loaded, pending, offset }) => {
@@ -1,11 +1,14 @@
1
1
  import { PAGE_SIZES } from './paging.js';
2
- /* Bump when the shape changes; a stored value of another version is ignored rather than migrated. `filters` was added alongside `where`, which still says the same thing for a value without it. */
2
+ /*
3
+ * Bump when the shape changes; a stored value of another version is ignored rather than migrated. `filters`
4
+ * came later without a bump, since `where` still says the same thing for a value without it.
5
+ */
3
6
  export const LAYOUT_VERSION = 1;
4
7
  export const EMPTY_LAYOUT = { widths: {}, hidden: [], pinned: [], pageSize: null, where: '', orderBy: '', filters: [] };
5
8
  export const layoutStorageKey = (connectionId, schema, table) => `database:table:${connectionId}:${schema}.${table}`;
6
9
  const isStringList = (value) => Array.isArray(value) && value.every((item) => typeof item === 'string');
7
- const isStoredFilter = (value) => isRecord(value) && typeof value.text === 'string' && typeof value.sql === 'string' && value.sql.trim() !== '';
8
10
  const isRecord = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
11
+ const isStoredFilter = (value) => isRecord(value) && typeof value.text === 'string' && typeof value.sql === 'string' && value.sql.trim() !== '';
9
12
  export const serializeLayout = (layout) => JSON.stringify({ version: LAYOUT_VERSION, ...layout });
10
13
  /* The layout a stored text holds, or `null` when it is missing, broken or from another version. A field that is off is dropped alone. */
11
14
  export const parseStoredLayout = (text) => {
@@ -69,7 +69,6 @@ export const rowKeyOf = (structure, loaded, row) => {
69
69
  }
70
70
  return key;
71
71
  };
72
- const numeric = (text) => Number(text);
73
72
  /* Updates, then deletes, then inserts, all computed against the page as loaded. Throws for a change on a row without a usable key, which the grid never lets a person make. */
74
73
  export const toRowChanges = (structure, loaded, pending) => {
75
74
  const keyOf = (row) => {
@@ -80,7 +79,7 @@ export const toRowChanges = (structure, loaded, pending) => {
80
79
  return key;
81
80
  };
82
81
  const updates = Object.keys(pending.edits)
83
- .map(numeric)
82
+ .map(Number)
84
83
  .filter((row) => !pending.deletes.has(row))
85
84
  .sort((left, right) => left - right)
86
85
  .map((row) => ({ kind: 'update', key: keyOf(row), values: pending.edits[row] }));
@@ -88,7 +87,16 @@ export const toRowChanges = (structure, loaded, pending) => {
88
87
  const inserts = pending.inserts.map((row) => ({ kind: 'insert', values: row.values }));
89
88
  return [...updates, ...deletes, ...inserts];
90
89
  };
90
+ const keyValueText = (value) => {
91
+ if (value === null) {
92
+ return 'NULL';
93
+ }
94
+ if (typeof value === 'object') {
95
+ return `0x${value.hex}`;
96
+ }
97
+ return typeof value === 'string' ? `'${value}'` : String(value);
98
+ };
91
99
  /* A key as a person would write it in a WHERE: `id = 5, tenant = 'a'`. */
92
100
  export const describeKey = (key) => Object.entries(key)
93
- .map(([column, value]) => `${column} = ${value === null ? 'NULL' : typeof value === 'object' ? `0x${value.hex}` : typeof value === 'string' ? `'${value}'` : String(value)}`)
101
+ .map(([column, value]) => `${column} = ${keyValueText(value)}`)
94
102
  .join(', ');
@@ -1,7 +1,9 @@
1
1
  import { useEffect, useRef, useState } from 'react';
2
2
  import { messageOf } from '@adecore/ui';
3
- import { DatabaseRequestError } from '../client/types.js';
3
+ import { isCancelled } from '../client/types.js';
4
4
  import { formatOfPath, importableColumns, mapColumn, matchColumns } from './import-mapping.js';
5
+ /* How long a finished export or import stays on the strip. */
6
+ const NOTICE_MS = 8000;
5
7
  /* Export of the table as filtered, and import of a file into it, with the state both need between the dialogs and the strip. */
6
8
  export function useTableTransfer({ client, session, files, schema, table, structure, where, orderBy, onImported, report }) {
7
9
  const [notice, setNotice] = useState(null);
@@ -13,7 +15,7 @@ export function useTableTransfer({ client, session, files, schema, table, struct
13
15
  if (notice?.kind !== 'exported' && notice?.kind !== 'imported') {
14
16
  return;
15
17
  }
16
- const timer = setTimeout(() => setNotice(null), 8000);
18
+ const timer = setTimeout(() => setNotice(null), NOTICE_MS);
17
19
  return () => clearTimeout(timer);
18
20
  }, [notice]);
19
21
  useEffect(() => () => sampling.current?.abort(), []);
@@ -43,8 +45,7 @@ export function useTableTransfer({ client, session, files, schema, table, struct
43
45
  show({ kind: 'exported', rows: result.rows });
44
46
  }
45
47
  catch (error) {
46
- const cancelled = controller.signal.aborted || (error instanceof DatabaseRequestError && error.code === 'cancelled');
47
- if (cancelled) {
48
+ if (controller.signal.aborted || isCancelled(error)) {
48
49
  setNotice(null);
49
50
  }
50
51
  else {
@@ -93,7 +94,7 @@ export function useTableTransfer({ client, session, files, schema, table, struct
93
94
  setImportDraft((now) => now === null || now.path !== path ? now : { ...now, header, sample: read, mapping: matchColumns(read.columns, tableColumns, header) });
94
95
  }
95
96
  catch (error) {
96
- if (!(error instanceof DatabaseRequestError && error.code === 'cancelled')) {
97
+ if (!isCancelled(error)) {
97
98
  setImportDraft((now) => (now === null || now.path !== path ? now : { ...now, error: messageOf(error) }));
98
99
  }
99
100
  }
@@ -32,12 +32,15 @@ const toTables = (database) => new Map(Object.entries(database.schemas).map(([sc
32
32
  }
33
33
  ]))
34
34
  ]));
35
- const findTable = (session, schema, name) => {
35
+ const findSchema = (session, schema) => {
36
36
  const tables = session.schemas.get(schema);
37
37
  if (tables === undefined) {
38
38
  throw new FakeError('query-failed', `Unknown database '${schema}'`, '42000');
39
39
  }
40
- const table = tables.get(name);
40
+ return tables;
41
+ };
42
+ const findTable = (session, schema, name) => {
43
+ const table = findSchema(session, schema).get(name);
41
44
  if (table === undefined) {
42
45
  throw new FakeError('query-failed', `Table '${schema}.${name}' doesn't exist`, '42S02');
43
46
  }
@@ -191,9 +194,7 @@ const selectSchema = (session, schema) => {
191
194
  if (schema === undefined) {
192
195
  return;
193
196
  }
194
- if (!session.schemas.has(schema)) {
195
- throw new FakeError('query-failed', `Unknown database '${schema}'`, '42000');
196
- }
197
+ findSchema(session, schema);
197
198
  session.schema = schema;
198
199
  };
199
200
  /*
@@ -246,10 +247,7 @@ export const fakeDatabaseTransport = (options) => {
246
247
  schemas: [...sessionOf(session).schemas.keys()].map((name) => ({ name, system: SYSTEM_SCHEMAS.includes(name) }))
247
248
  }),
248
249
  tables: ({ session, schema }) => {
249
- const tables = sessionOf(session).schemas.get(schema);
250
- if (tables === undefined) {
251
- throw new FakeError('query-failed', `Unknown database '${schema}'`, '42000');
252
- }
250
+ const tables = findSchema(sessionOf(session), schema);
253
251
  return { tables: [...tables].map(([name, table]) => ({ name, kind: table.kind, rowEstimate: table.rows.length, comment: null })) };
254
252
  },
255
253
  structure: ({ session, schema, table: name }) => {
@@ -69,7 +69,7 @@ export function ValueEditor({ column, value, editable, onCommit }) {
69
69
  copyText(rendered.text);
70
70
  }
71
71
  else {
72
- copyText(draft.mode === 'value' ? draft.text : draft.mode === 'null' ? 'NULL' : 'DEFAULT');
72
+ copyText(placeholder ?? draft.text);
73
73
  }
74
74
  };
75
75
  const editor = (extra) => (_jsx(TextArea, { "aria-label": t(source.binary ? 'value.hexEditor' : 'value.editor', { column: column.name }), wrap: wrap || source.binary ? 'soft' : 'off', spellCheck: false, readOnly: !editable, placeholder: placeholder, value: draft.mode === 'value' ? draft.text : '', className: clsx(CODE_TEXT, extra), onChange: (event) => change({ text: event.target.value, mode: 'value' }), onKeyDown: handleKeyDown }));
@@ -2,12 +2,7 @@ import { parseDraft } from '../grid/edit-value.js';
2
2
  import { parseHex } from './hex.js';
3
3
  import { checkJson, isJsonDocument } from './json.js';
4
4
  /* The text a value is edited as: a binary value as its hex, everything else as it is written. */
5
- const textOf = (value) => {
6
- if (typeof value === 'object') {
7
- return value.hex;
8
- }
9
- return String(value);
10
- };
5
+ const textOf = (value) => (typeof value === 'object' ? value.hex : String(value));
11
6
  export const sourceOf = (column, value) => {
12
7
  const binary = column.kind === 'binary' || (value !== null && typeof value === 'object');
13
8
  const original = value === null ? { text: '', mode: 'null' } : { text: textOf(value), mode: 'value' };
@@ -42,11 +42,11 @@ export const prettyJson = (text) => {
42
42
  continue;
43
43
  }
44
44
  if (char === '{' || char === '[') {
45
- const empty = nextToken(i + 1) === (char === '{' ? '}' : ']');
45
+ const close = char === '{' ? '}' : ']';
46
46
  output += char;
47
- if (empty) {
48
- output += char === '{' ? '}' : ']';
49
- i = text.indexOf(char === '{' ? '}' : ']', i + 1) + 1;
47
+ if (nextToken(i + 1) === close) {
48
+ output += close;
49
+ i = text.indexOf(close, i + 1) + 1;
50
50
  continue;
51
51
  }
52
52
  depth++;