@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/table/TableView.tsx
CHANGED
|
@@ -29,6 +29,7 @@ import { RecordView } from '../grid/RecordView.tsx';
|
|
|
29
29
|
import { ValueDock } from '../grid/ValueDock.tsx';
|
|
30
30
|
import { valueOfCell, type EditValue, type RowChange, type TableStructure, type Value } from '../protocol/index.ts';
|
|
31
31
|
import type { SqlTarget } from '../sql.ts';
|
|
32
|
+
import { parseStoredSize } from '../stored-size.ts';
|
|
32
33
|
import { useSchemaChange } from '../use-schema-change.ts';
|
|
33
34
|
import { useStableCallback } from '../use-stable-callback.ts';
|
|
34
35
|
import {
|
|
@@ -78,15 +79,6 @@ import { useTableTransfer, type TransferNotice } from './useTableTransfer.ts';
|
|
|
78
79
|
export const RECORD_WIDTH_KEY = 'database:record-view';
|
|
79
80
|
const RECORD_WIDTH = 360;
|
|
80
81
|
|
|
81
|
-
const parseRecordWidth = (raw: string | null | undefined): number | null => {
|
|
82
|
-
try {
|
|
83
|
-
const value: unknown = JSON.parse(raw ?? 'null');
|
|
84
|
-
return typeof value === 'number' && Number.isFinite(value) && value > 0 ? Math.round(value) : null;
|
|
85
|
-
} catch {
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
|
-
|
|
90
82
|
export interface TableViewProps {
|
|
91
83
|
connection: Connection;
|
|
92
84
|
schema: string;
|
|
@@ -113,12 +105,15 @@ const readOnlyReasonOf = (connection: Connection, structure: TableStructure | nu
|
|
|
113
105
|
if (connection.config.readOnly === true) {
|
|
114
106
|
return t('table.readOnly.connection');
|
|
115
107
|
}
|
|
108
|
+
|
|
116
109
|
if (structure?.kind === 'view') {
|
|
117
110
|
return t('table.readOnly.view');
|
|
118
111
|
}
|
|
112
|
+
|
|
119
113
|
if (structure !== null && structure.rowKey === null) {
|
|
120
114
|
return t('table.readOnly.noKey');
|
|
121
115
|
}
|
|
116
|
+
|
|
122
117
|
return null;
|
|
123
118
|
};
|
|
124
119
|
|
|
@@ -178,6 +173,7 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
|
|
|
178
173
|
const counter = useRef<AbortController | null>(null);
|
|
179
174
|
const insertedBefore = useRef(0);
|
|
180
175
|
const engine = connection.config.engine;
|
|
176
|
+
|
|
181
177
|
const [chips, setChips] = useState<Chip[]>(() =>
|
|
182
178
|
chipsFromQuery(engine, {
|
|
183
179
|
where: defaultWhere ?? stored?.where ?? '',
|
|
@@ -185,6 +181,7 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
|
|
|
185
181
|
remembered: defaultWhere === undefined ? stored?.filters : undefined
|
|
186
182
|
})
|
|
187
183
|
);
|
|
184
|
+
|
|
188
185
|
const [offset, setOffset] = useState(0);
|
|
189
186
|
const [pageSize, setPageSize] = useState(stored?.pageSize ?? DEFAULT_PAGE_SIZE);
|
|
190
187
|
const [pending, setPending] = useState<PendingChanges>(emptyPending);
|
|
@@ -197,7 +194,7 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
|
|
|
197
194
|
const [discard, setDiscard] = useState<{ run(): void } | null>(null);
|
|
198
195
|
const [focus, setFocus] = useState<FocusedCell | null>(null);
|
|
199
196
|
const [recordOpen, setRecordOpen] = useState(false);
|
|
200
|
-
const [recordWidth, setRecordWidth] = useState(() =>
|
|
197
|
+
const [recordWidth, setRecordWidth] = useState(() => parseStoredSize(storage?.get(RECORD_WIDTH_KEY), Number.MIN_VALUE));
|
|
201
198
|
const [focusRequest, setFocusRequest] = useState<FocusRequest | null>(null);
|
|
202
199
|
const [columnRequest, setColumnRequest] = useState<ColumnRequest | null>(null);
|
|
203
200
|
const [range, setRange] = useState<RangeBlock | null>(null);
|
|
@@ -205,6 +202,7 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
|
|
|
205
202
|
const applied = useMemo((): Filters => ({ where: chipsToWhere(chips), orderBy: chipsToOrderBy(engine, chips) }), [chips, engine]);
|
|
206
203
|
const target = `${connection.id}|${schema}|${table}`;
|
|
207
204
|
const structureLoad = useLoaded((signal) => session.structure(schema, table, { signal }), target);
|
|
205
|
+
|
|
208
206
|
// The offset travels with the page it belongs to, since the page on screen lags behind the one asked for.
|
|
209
207
|
const rowsLoad = useLoaded(
|
|
210
208
|
async (signal) => ({
|
|
@@ -218,6 +216,7 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
|
|
|
218
216
|
}),
|
|
219
217
|
JSON.stringify([target, applied, offset, pageSize])
|
|
220
218
|
);
|
|
219
|
+
|
|
221
220
|
const structure = structureLoad.value;
|
|
222
221
|
const loaded = rowsLoad.value?.result ?? null;
|
|
223
222
|
const loadedOffset = rowsLoad.value?.offset ?? 0;
|
|
@@ -226,10 +225,12 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
|
|
|
226
225
|
const editable = readOnlyReason === null && structure !== null && loaded !== null && !rowsLoad.loading;
|
|
227
226
|
|
|
228
227
|
const columns = useMemo(() => (loaded === null ? [] : buildGridColumns(loaded, structure)), [loaded, structure]);
|
|
228
|
+
|
|
229
229
|
const rows = useMemo(
|
|
230
230
|
() => (loaded === null ? [] : buildGridRows({ structure, loaded, pending, offset: loadedOffset })),
|
|
231
231
|
[loaded, structure, pending, loadedOffset]
|
|
232
232
|
);
|
|
233
|
+
|
|
233
234
|
const transfer = useTableTransfer({
|
|
234
235
|
client,
|
|
235
236
|
session,
|
|
@@ -257,6 +258,7 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
|
|
|
257
258
|
: { tone: 'success', title: transferText(found, t) }
|
|
258
259
|
)
|
|
259
260
|
});
|
|
261
|
+
|
|
260
262
|
const changeCount = pendingCount(pending);
|
|
261
263
|
const dirty = changeCount > 0;
|
|
262
264
|
const reportDirty = useRef(onDirtyChange);
|
|
@@ -267,6 +269,7 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
|
|
|
267
269
|
// The record view shows the row of the focused cell, or the first row before a cell has the focus.
|
|
268
270
|
const focusedIndex = focus === null ? -1 : rows.findIndex((row) => row.key === focus.rowKey);
|
|
269
271
|
const recordIndex = focusedIndex >= 0 ? focusedIndex : rows.length > 0 ? 0 : -1;
|
|
272
|
+
|
|
270
273
|
const selectionFigures = useMemo(
|
|
271
274
|
() =>
|
|
272
275
|
range === null || blockSize(range) < 2
|
|
@@ -281,6 +284,7 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
|
|
|
281
284
|
},
|
|
282
285
|
[range, rows, columns]
|
|
283
286
|
);
|
|
287
|
+
|
|
284
288
|
const sqlTarget = useMemo((): SqlTarget => ({ engine, schema, table }), [engine, schema, table]);
|
|
285
289
|
|
|
286
290
|
useSchemaChange(connection.id, schema, () => {
|
|
@@ -307,6 +311,7 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
|
|
|
307
311
|
useEffect(() => {
|
|
308
312
|
const controller = new AbortController();
|
|
309
313
|
lifetime.current = controller;
|
|
314
|
+
|
|
310
315
|
return () => {
|
|
311
316
|
controller.abort();
|
|
312
317
|
counter.current?.abort();
|
|
@@ -317,6 +322,7 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
|
|
|
317
322
|
if (pending.inserts.length > insertedBefore.current && grid.current !== null) {
|
|
318
323
|
grid.current.scrollTop = grid.current.scrollHeight;
|
|
319
324
|
}
|
|
325
|
+
|
|
320
326
|
insertedBefore.current = pending.inserts.length;
|
|
321
327
|
}, [pending.inserts.length]);
|
|
322
328
|
|
|
@@ -350,11 +356,14 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
|
|
|
350
356
|
const where = chipsToWhere(next);
|
|
351
357
|
const orderBy = chipsToOrderBy(engine, next);
|
|
352
358
|
const filters = next.filter((chip): chip is FilterChip => chip.kind === 'filter').map(({ text, sql }) => ({ text, sql }));
|
|
359
|
+
|
|
353
360
|
if (where === applied.where && orderBy === applied.orderBy) {
|
|
354
361
|
remember({ filters });
|
|
355
362
|
setChips([...next]);
|
|
363
|
+
|
|
356
364
|
return;
|
|
357
365
|
}
|
|
366
|
+
|
|
358
367
|
guard(() => {
|
|
359
368
|
remember({ where, orderBy, filters });
|
|
360
369
|
counter.current?.abort();
|
|
@@ -375,11 +384,13 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
|
|
|
375
384
|
const row = parseRowKey(cell.rowKey);
|
|
376
385
|
const column = loaded?.columns[cell.column];
|
|
377
386
|
const value = loaded === null || row.kind !== 'loaded' ? undefined : valueOfCell(loaded.rows[row.index]?.[cell.column] ?? null);
|
|
387
|
+
|
|
378
388
|
return column === undefined || value === undefined ? null : cellChip(engine, column.name, value, column.kind, exclude);
|
|
379
389
|
};
|
|
380
390
|
|
|
381
391
|
const sortByColumn = (cell: FocusedCell): void => {
|
|
382
392
|
const name = columns[cell.column]?.name;
|
|
393
|
+
|
|
383
394
|
if (name !== undefined) {
|
|
384
395
|
applyChips(toggleSort(chips, name));
|
|
385
396
|
}
|
|
@@ -387,6 +398,7 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
|
|
|
387
398
|
|
|
388
399
|
const jumpToColumn = (name: string): void => {
|
|
389
400
|
const index = columns.findIndex((column) => column.name === name);
|
|
401
|
+
|
|
390
402
|
if (index >= 0) {
|
|
391
403
|
setRecordOpen(false);
|
|
392
404
|
setColumnRequest({ column: index });
|
|
@@ -423,10 +435,13 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
|
|
|
423
435
|
counter.current?.abort();
|
|
424
436
|
counter.current = controller;
|
|
425
437
|
setCounting(true);
|
|
438
|
+
|
|
426
439
|
try {
|
|
427
440
|
const total = await session.count(schema, table, applied.where || undefined, { signal: controller.signal });
|
|
441
|
+
|
|
428
442
|
if (!controller.signal.aborted) {
|
|
429
443
|
setCounted(total);
|
|
444
|
+
|
|
430
445
|
return total;
|
|
431
446
|
}
|
|
432
447
|
} catch (error) {
|
|
@@ -438,11 +453,13 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
|
|
|
438
453
|
setCounting(false);
|
|
439
454
|
}
|
|
440
455
|
}
|
|
456
|
+
|
|
441
457
|
return null;
|
|
442
458
|
};
|
|
443
459
|
|
|
444
460
|
const goToLast = async (): Promise<void> => {
|
|
445
461
|
const total = counted ?? (await countRows());
|
|
462
|
+
|
|
446
463
|
if (total !== null) {
|
|
447
464
|
goToPage(lastPageOffset(total, pageSize));
|
|
448
465
|
}
|
|
@@ -451,9 +468,11 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
|
|
|
451
468
|
const commit = (rowKey: string, column: number, value: EditValue): void => {
|
|
452
469
|
const name = loaded?.columns[column]?.name;
|
|
453
470
|
const row = parseRowKey(rowKey);
|
|
471
|
+
|
|
454
472
|
if (loaded === null || name === undefined) {
|
|
455
473
|
return;
|
|
456
474
|
}
|
|
475
|
+
|
|
457
476
|
if (row.kind === 'inserted') {
|
|
458
477
|
setPending((now) => setInsertValue(now, row.id, name, value));
|
|
459
478
|
} else {
|
|
@@ -465,13 +484,16 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
|
|
|
465
484
|
const row = parseRowKey(rowKey);
|
|
466
485
|
const name = loaded?.columns[column]?.name;
|
|
467
486
|
const key = row.kind === 'loaded' && structure !== null && loaded !== null ? rowKeyOf(structure, loaded, row.index) : null;
|
|
487
|
+
|
|
468
488
|
if (key === null || name === undefined) {
|
|
469
489
|
return undefined;
|
|
470
490
|
}
|
|
491
|
+
|
|
471
492
|
try {
|
|
472
493
|
return await session.cell(schema, table, key, name, { signal: lifetime.current?.signal });
|
|
473
494
|
} catch (error) {
|
|
474
495
|
setFailure(messageOf(error));
|
|
496
|
+
|
|
475
497
|
return undefined;
|
|
476
498
|
}
|
|
477
499
|
});
|
|
@@ -479,14 +501,17 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
|
|
|
479
501
|
const refsOf = (keys: Iterable<string>): RowSelectionRefs => {
|
|
480
502
|
const loadedRows: number[] = [];
|
|
481
503
|
const inserted: number[] = [];
|
|
504
|
+
|
|
482
505
|
for (const key of keys) {
|
|
483
506
|
const row = parseRowKey(key);
|
|
507
|
+
|
|
484
508
|
if (row.kind === 'inserted') {
|
|
485
509
|
inserted.push(row.id);
|
|
486
510
|
} else {
|
|
487
511
|
loadedRows.push(row.index);
|
|
488
512
|
}
|
|
489
513
|
}
|
|
514
|
+
|
|
490
515
|
return { loaded: loadedRows, inserted };
|
|
491
516
|
};
|
|
492
517
|
|
|
@@ -494,11 +519,13 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
|
|
|
494
519
|
setPending((now) => {
|
|
495
520
|
const refs = refsOf(keys);
|
|
496
521
|
const next = refs.inserted.reduce(removeInsert, now);
|
|
522
|
+
|
|
497
523
|
return markDeleted(
|
|
498
524
|
next,
|
|
499
525
|
refs.loaded.filter((index) => loaded !== null && structure !== null && rowKeyOf(structure, loaded, index) !== null)
|
|
500
526
|
);
|
|
501
527
|
});
|
|
528
|
+
|
|
502
529
|
setSelected(NO_KEYS);
|
|
503
530
|
};
|
|
504
531
|
|
|
@@ -520,16 +547,19 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
|
|
|
520
547
|
const column = columns[cell.column];
|
|
521
548
|
const key = column === undefined || structure === null ? undefined : foreignKeyOf(structure.foreignKeys, column.name);
|
|
522
549
|
const row = rowByKey.get(cell.rowKey);
|
|
550
|
+
|
|
523
551
|
return key === undefined || row === undefined ? null : referenceOf(engine, key, columns, row.cells);
|
|
524
552
|
};
|
|
525
553
|
|
|
526
554
|
const isReferenceColumn = (column: number): boolean => {
|
|
527
555
|
const name = columns[column]?.name;
|
|
556
|
+
|
|
528
557
|
return name !== undefined && structure !== null && foreignKeyOf(structure.foreignKeys, name) !== undefined;
|
|
529
558
|
};
|
|
530
559
|
|
|
531
560
|
const follow = (cell: FocusedCell): void => {
|
|
532
561
|
const reference = referenceAt(cell);
|
|
562
|
+
|
|
533
563
|
if (reference !== null) {
|
|
534
564
|
onAction?.({
|
|
535
565
|
kind: 'open-table',
|
|
@@ -552,6 +582,7 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
|
|
|
552
582
|
|
|
553
583
|
const changeRecordIndex = (index: number): void => {
|
|
554
584
|
const row = rows[index];
|
|
585
|
+
|
|
555
586
|
if (row !== undefined) {
|
|
556
587
|
followInGrid({ rowKey: row.key, column: focus?.column ?? 0 });
|
|
557
588
|
}
|
|
@@ -560,11 +591,14 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
|
|
|
560
591
|
const queryMenu = (context: GridMenuContext): ReactNode => {
|
|
561
592
|
const cell = context.cell;
|
|
562
593
|
const name = cell === null ? undefined : columns[cell.column]?.name;
|
|
594
|
+
|
|
563
595
|
if (cell === null || name === undefined) {
|
|
564
596
|
return null;
|
|
565
597
|
}
|
|
598
|
+
|
|
566
599
|
const include = cellChipAt(cell, false);
|
|
567
600
|
const exclude = cellChipAt(cell, true);
|
|
601
|
+
|
|
568
602
|
return (
|
|
569
603
|
<>
|
|
570
604
|
<ContextMenu.Item disabled={include === null} onClick={() => include !== null && applyChips(addChip(chips, include))}>
|
|
@@ -591,6 +625,7 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
|
|
|
591
625
|
|
|
592
626
|
const gridMenu = (context: GridMenuContext): ReactNode => {
|
|
593
627
|
const count = context.rowKeys.length;
|
|
628
|
+
|
|
594
629
|
return (
|
|
595
630
|
<>
|
|
596
631
|
<ContextMenu.Item disabled={!editable} onClick={() => cloneRows(context.rowKeys)}>
|
|
@@ -615,9 +650,11 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
|
|
|
615
650
|
|
|
616
651
|
const conflictMessage = (error: DatabaseRequestError, changes: readonly RowChange[]): string => {
|
|
617
652
|
const change = error.change === undefined ? undefined : changes[error.change];
|
|
653
|
+
|
|
618
654
|
if (error.change === undefined || change === undefined || change.kind === 'insert') {
|
|
619
655
|
return error.message;
|
|
620
656
|
}
|
|
657
|
+
|
|
621
658
|
return t('table.conflict', { number: formatNumber(error.change + 1), row: describeKey(change.key) });
|
|
622
659
|
};
|
|
623
660
|
|
|
@@ -625,9 +662,11 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
|
|
|
625
662
|
if (structure === null || loaded === null) {
|
|
626
663
|
return;
|
|
627
664
|
}
|
|
665
|
+
|
|
628
666
|
const changes = toRowChanges(structure, loaded, pending);
|
|
629
667
|
setSubmitting(true);
|
|
630
668
|
setFailure(null);
|
|
669
|
+
|
|
631
670
|
try {
|
|
632
671
|
await session.apply(schema, table, changes);
|
|
633
672
|
discardPending();
|
|
@@ -834,6 +873,7 @@ interface TransferBannerProps {
|
|
|
834
873
|
|
|
835
874
|
function TransferBanner({ notice, onCancel, onDismiss }: TransferBannerProps) {
|
|
836
875
|
const { t } = useTranslation('database');
|
|
876
|
+
|
|
837
877
|
return (
|
|
838
878
|
<Banner
|
|
839
879
|
icon={notice.kind === 'failed' ? CircleAlert : notice.kind === 'exporting' ? Download : Check}
|
|
@@ -56,6 +56,7 @@ const PATTERN = /^((?:NOT\s+)?(?:LIKE|ILIKE|GLOB|REGEXP|RLIKE))\s+([\s\S]+)$/i;
|
|
|
56
56
|
const MEMBER = /^((?:NOT\s+)?IN)\s*(\([\s\S]+\))$/i;
|
|
57
57
|
const BETWEEN = /^((?:NOT\s+)?BETWEEN)\s+([\s\S]+?)\s+AND\s+([\s\S]+)$/i;
|
|
58
58
|
const IS_TEST = /^IS\s+(?:NOT\s+)?(?:NULL|TRUE|FALSE)$/i;
|
|
59
|
+
|
|
59
60
|
/* A name being typed, or one that opened a quote. Whitespace after it means an operator is coming. */
|
|
60
61
|
const NAME_BEING_TYPED = /^\s*(?:[\w$]*|[`"][^`"]*[`"]?)$/;
|
|
61
62
|
|
|
@@ -76,9 +77,11 @@ export const chipsToWhere = (chips: readonly Chip[]): string => {
|
|
|
76
77
|
.filter(isFilter)
|
|
77
78
|
.map((chip) => chip.sql.trim())
|
|
78
79
|
.filter((sql) => sql !== '');
|
|
80
|
+
|
|
79
81
|
if (conditions.length < 2) {
|
|
80
82
|
return conditions[0] ?? '';
|
|
81
83
|
}
|
|
84
|
+
|
|
82
85
|
return conditions.map((sql) => (OR.test(sql) ? `(${sql})` : sql)).join(' AND ');
|
|
83
86
|
};
|
|
84
87
|
|
|
@@ -89,6 +92,7 @@ export const chipsToOrderBy = (engine: Engine, chips: readonly Chip[]): string =
|
|
|
89
92
|
if (chip.kind === 'sort') {
|
|
90
93
|
return [orderByClause(engine, [chip])];
|
|
91
94
|
}
|
|
95
|
+
|
|
92
96
|
return chip.kind === 'order' ? [chip.text.trim()] : [];
|
|
93
97
|
})
|
|
94
98
|
.filter((part) => part !== '')
|
|
@@ -104,6 +108,7 @@ export const toggleSort = (chips: readonly Chip[], column: string): Chip[] => {
|
|
|
104
108
|
if (!chips.some((chip) => isSort(chip) && chip.column === column)) {
|
|
105
109
|
return [...chips, { kind: 'sort', column, direction: 'asc' }];
|
|
106
110
|
}
|
|
111
|
+
|
|
107
112
|
return chips.map((chip) => (isSort(chip) && chip.column === column ? { ...chip, direction: chip.direction === 'asc' ? 'desc' : 'asc' } : chip));
|
|
108
113
|
};
|
|
109
114
|
|
|
@@ -116,6 +121,7 @@ export const removeChipAt = (chips: readonly Chip[], index: number): Chip[] => c
|
|
|
116
121
|
/* Puts the chip where another stood, dropping it if an equal one is already elsewhere in the list. */
|
|
117
122
|
export const replaceChipAt = (chips: readonly Chip[], index: number, chip: Chip): Chip[] => {
|
|
118
123
|
const duplicate = chips.some((other, at) => at !== index && chipKey(other) === chipKey(chip));
|
|
124
|
+
|
|
119
125
|
return duplicate ? removeChipAt(chips, index) : chips.map((other, at) => (at === index ? chip : other));
|
|
120
126
|
};
|
|
121
127
|
|
|
@@ -124,20 +130,26 @@ const rawFilter = (text: string): FilterChip => ({ kind: 'filter', text, sql: te
|
|
|
124
130
|
/* The WHERE of a layout or of a jump as chips: the remembered ones when they still say that text, else the text as one raw chip. */
|
|
125
131
|
export const filterChipsOf = (where: string, remembered: readonly StoredFilter[] = []): Chip[] => {
|
|
126
132
|
const text = where.trim();
|
|
133
|
+
|
|
127
134
|
if (text === '') {
|
|
128
135
|
return [];
|
|
129
136
|
}
|
|
137
|
+
|
|
130
138
|
const kept = remembered.map((filter): FilterChip => ({ kind: 'filter', text: filter.text, sql: filter.sql }));
|
|
139
|
+
|
|
131
140
|
return chipsToWhere(kept) === text ? kept : [rawFilter(text)];
|
|
132
141
|
};
|
|
133
142
|
|
|
134
143
|
/* A plain list of columns becomes sort chips; anything else is one chip that keeps the text. */
|
|
135
144
|
export const sortChipsOf = (engine: Engine, orderBy: string): Chip[] => {
|
|
136
145
|
const text = orderBy.trim();
|
|
146
|
+
|
|
137
147
|
if (text === '') {
|
|
138
148
|
return [];
|
|
139
149
|
}
|
|
150
|
+
|
|
140
151
|
const sorts = parseOrderByLoose(engine, text);
|
|
152
|
+
|
|
141
153
|
return sorts === null ? [{ kind: 'order', text }] : sorts.map(sortChip);
|
|
142
154
|
};
|
|
143
155
|
|
|
@@ -156,6 +168,7 @@ export const chipsFromQuery = (engine: Engine, { where, orderBy, remembered }: C
|
|
|
156
168
|
const isClosed = (operand: string): boolean => {
|
|
157
169
|
let quote: string | null = null;
|
|
158
170
|
let depth = 0;
|
|
171
|
+
|
|
159
172
|
for (const character of operand) {
|
|
160
173
|
if (quote !== null) {
|
|
161
174
|
quote = character === quote ? null : quote;
|
|
@@ -165,11 +178,13 @@ const isClosed = (operand: string): boolean => {
|
|
|
165
178
|
depth++;
|
|
166
179
|
} else if (character === ')') {
|
|
167
180
|
depth--;
|
|
181
|
+
|
|
168
182
|
if (depth < 0) {
|
|
169
183
|
return false;
|
|
170
184
|
}
|
|
171
185
|
}
|
|
172
186
|
}
|
|
187
|
+
|
|
173
188
|
return quote === null && depth === 0;
|
|
174
189
|
};
|
|
175
190
|
|
|
@@ -178,20 +193,27 @@ const upper = (keyword: string): string => keyword.replace(/\s+/g, ' ').toUpperC
|
|
|
178
193
|
/* The operator and operands of a condition in the form the chip shows, or `null` when the text does not read as one. */
|
|
179
194
|
const readOperation = (rest: string): string | null => {
|
|
180
195
|
const compare = COMPARE.exec(rest);
|
|
196
|
+
|
|
181
197
|
if (compare !== null) {
|
|
182
198
|
const operator = compare[1] === '==' ? '=' : compare[1] === '!=' ? '<>' : compare[1];
|
|
199
|
+
|
|
183
200
|
return isClosed(compare[2]!) ? `${operator} ${compare[2]}` : null;
|
|
184
201
|
}
|
|
202
|
+
|
|
185
203
|
for (const form of [PATTERN, MEMBER]) {
|
|
186
204
|
const found = form.exec(rest);
|
|
205
|
+
|
|
187
206
|
if (found !== null) {
|
|
188
207
|
return isClosed(found[2]!) ? `${upper(found[1]!)} ${found[2]}` : null;
|
|
189
208
|
}
|
|
190
209
|
}
|
|
210
|
+
|
|
191
211
|
const between = BETWEEN.exec(rest);
|
|
212
|
+
|
|
192
213
|
if (between !== null) {
|
|
193
214
|
return isClosed(between[2]!) && isClosed(between[3]!) ? `${upper(between[1]!)} ${between[2]} AND ${between[3]}` : null;
|
|
194
215
|
}
|
|
216
|
+
|
|
195
217
|
return IS_TEST.test(rest) ? upper(rest) : null;
|
|
196
218
|
};
|
|
197
219
|
|
|
@@ -201,14 +223,18 @@ const readOperation = (rest: string): string | null => {
|
|
|
201
223
|
*/
|
|
202
224
|
export const parseCondition = (engine: Engine, text: string, columns: readonly string[]): FilterChip | null => {
|
|
203
225
|
const match = WORD.exec(text.trim());
|
|
226
|
+
|
|
204
227
|
if (match === null) {
|
|
205
228
|
return null;
|
|
206
229
|
}
|
|
230
|
+
|
|
207
231
|
const column = findColumn(engine, match[1]!, columns);
|
|
208
232
|
const operation = column === undefined ? null : readOperation(match[2]!.trim());
|
|
233
|
+
|
|
209
234
|
if (column === undefined || operation === null) {
|
|
210
235
|
return null;
|
|
211
236
|
}
|
|
237
|
+
|
|
212
238
|
return { kind: 'filter', text: `${match[1]} ${operation}`, sql: `${quoteIdentifier(engine, column)} ${operation}` };
|
|
213
239
|
};
|
|
214
240
|
|
|
@@ -216,6 +242,7 @@ export const parseCondition = (engine: Engine, text: string, columns: readonly s
|
|
|
216
242
|
export const cellChip = (engine: Engine, column: string, value: LiteralValue, kind: ValueKind | undefined, exclude: boolean): FilterChip => {
|
|
217
243
|
const name = identifierText(column);
|
|
218
244
|
const text = value === null ? `${name} IS ${exclude ? 'NOT ' : ''}NULL` : `${name} ${exclude ? '<>' : '='} ${sqlLiteral(engine, value, kind)}`;
|
|
245
|
+
|
|
219
246
|
return { kind: 'filter', text, sql: exclude ? notEqualsCondition(engine, column, value, kind) : equalsCondition(engine, column, value, kind) };
|
|
220
247
|
};
|
|
221
248
|
|
|
@@ -225,24 +252,30 @@ export const shorten = (text: string, limit: number): string => (text.length > l
|
|
|
225
252
|
/* How well a name matches what is typed: 0 is the name itself, then its start, a word of it, anywhere in it; `null` is no match. */
|
|
226
253
|
const rankOf = (name: string, token: string): number | null => {
|
|
227
254
|
const lower = name.toLowerCase();
|
|
255
|
+
|
|
228
256
|
if (lower === token) {
|
|
229
257
|
return 0;
|
|
230
258
|
}
|
|
259
|
+
|
|
231
260
|
if (lower.startsWith(token)) {
|
|
232
261
|
return 1;
|
|
233
262
|
}
|
|
263
|
+
|
|
234
264
|
if (lower.split(/[\s_.-]+/).some((word) => word.startsWith(token))) {
|
|
235
265
|
return 2;
|
|
236
266
|
}
|
|
267
|
+
|
|
237
268
|
return lower.includes(token) ? 3 : null;
|
|
238
269
|
};
|
|
239
270
|
|
|
240
271
|
/* The columns that match a typed name, the closest first and in table order among equals. */
|
|
241
272
|
export const matchColumns = (columns: readonly CommandColumn[], typed: string, limit = SUGGESTED_COLUMNS): CommandColumn[] => {
|
|
242
273
|
const token = typed.trim().replace(/^[`"]/, '').replace(/[`"]$/, '').toLowerCase();
|
|
274
|
+
|
|
243
275
|
if (token === '') {
|
|
244
276
|
return [];
|
|
245
277
|
}
|
|
278
|
+
|
|
246
279
|
return columns
|
|
247
280
|
.map((column, order) => ({ column, order, rank: rankOf(column.name, token) }))
|
|
248
281
|
.filter((entry): entry is { column: CommandColumn; order: number; rank: number } => entry.rank !== null)
|
|
@@ -257,24 +290,31 @@ export const matchColumns = (columns: readonly CommandColumn[], typed: string, l
|
|
|
257
290
|
*/
|
|
258
291
|
export const suggest = (engine: Engine, text: string, columns: readonly CommandColumn[], limit = SUGGESTED_COLUMNS): Suggestion[] => {
|
|
259
292
|
const typed = text.trim();
|
|
293
|
+
|
|
260
294
|
if (typed === '') {
|
|
261
295
|
return [];
|
|
262
296
|
}
|
|
297
|
+
|
|
263
298
|
const suggestions: Suggestion[] = [];
|
|
299
|
+
|
|
264
300
|
const condition = parseCondition(
|
|
265
301
|
engine,
|
|
266
302
|
typed,
|
|
267
303
|
columns.map((column) => column.name)
|
|
268
304
|
);
|
|
305
|
+
|
|
269
306
|
if (condition !== null) {
|
|
270
307
|
suggestions.push({ kind: 'condition', chip: condition });
|
|
271
308
|
}
|
|
309
|
+
|
|
272
310
|
if (NAME_BEING_TYPED.test(text)) {
|
|
273
311
|
for (const column of matchColumns(columns, typed, limit)) {
|
|
274
312
|
suggestions.push({ kind: 'filter', column }, { kind: 'sort', column }, { kind: 'jump', column });
|
|
275
313
|
}
|
|
276
314
|
}
|
|
315
|
+
|
|
277
316
|
suggestions.push({ kind: 'sql', chip: rawFilter(typed) });
|
|
317
|
+
|
|
278
318
|
return suggestions;
|
|
279
319
|
};
|
|
280
320
|
|
package/src/table/grid-rows.ts
CHANGED
|
@@ -15,7 +15,9 @@ export const parseRowKey = (key: string): RowRef =>
|
|
|
15
15
|
|
|
16
16
|
export const buildGridColumns = (loaded: Pick<RowsResult, 'columns'>, structure: TableStructure | null): GridColumn[] => {
|
|
17
17
|
const keys = { primaryKey: structure?.primaryKey ?? [], foreignKey: structure?.foreignKeys.flatMap((key) => key.columns) ?? [] };
|
|
18
|
+
|
|
18
19
|
const infoOf = (name: string) => structure?.columns.find((info) => info.name === name);
|
|
20
|
+
|
|
19
21
|
return loaded.columns.map((column) => gridColumnOf(column, infoOf(column.name), keys));
|
|
20
22
|
};
|
|
21
23
|
|
|
@@ -32,14 +34,19 @@ export const buildGridRows = ({ structure, loaded, pending, offset }: GridRowsIn
|
|
|
32
34
|
const rows = loaded.rows.map((cells, index): GridRow => {
|
|
33
35
|
const edits = pending.edits[index];
|
|
34
36
|
const edited = new Set<number>();
|
|
37
|
+
|
|
35
38
|
const shown = cells.map((cell, column): Shown => {
|
|
36
39
|
const edit = edits?.[loaded.columns[column]!.name];
|
|
40
|
+
|
|
37
41
|
if (edit === undefined) {
|
|
38
42
|
return cell;
|
|
39
43
|
}
|
|
44
|
+
|
|
40
45
|
edited.add(column);
|
|
46
|
+
|
|
41
47
|
return shownOfEdit(edit);
|
|
42
48
|
});
|
|
49
|
+
|
|
43
50
|
return {
|
|
44
51
|
key: loadedRowKey(index),
|
|
45
52
|
number: offset + index + 1,
|
|
@@ -49,15 +56,18 @@ export const buildGridRows = ({ structure, loaded, pending, offset }: GridRowsIn
|
|
|
49
56
|
locked: structure === null || rowKeyOf(structure, loaded, index) === null
|
|
50
57
|
};
|
|
51
58
|
});
|
|
59
|
+
|
|
52
60
|
const inserted = pending.inserts.map((row): GridRow => ({
|
|
53
61
|
key: insertedRowKey(row.id),
|
|
54
62
|
number: null,
|
|
55
63
|
cells: loaded.columns.map((column): Shown => {
|
|
56
64
|
const value = row.values[column.name];
|
|
65
|
+
|
|
57
66
|
return value === undefined ? { kind: 'default' } : shownOfEdit(value);
|
|
58
67
|
}),
|
|
59
68
|
state: 'inserted'
|
|
60
69
|
}));
|
|
70
|
+
|
|
61
71
|
return [...rows, ...inserted];
|
|
62
72
|
};
|
|
63
73
|
|
|
@@ -67,11 +77,14 @@ export const buildGridRows = ({ structure, loaded, pending, offset }: GridRowsIn
|
|
|
67
77
|
*/
|
|
68
78
|
export const cloneValues = (columns: readonly GridColumn[], cells: readonly Shown[]): Record<string, EditValue> => {
|
|
69
79
|
const values: Record<string, EditValue> = {};
|
|
80
|
+
|
|
70
81
|
columns.forEach((column, index) => {
|
|
71
82
|
const value = column.autoIncrement === true || column.readOnly === true ? undefined : wholeValueOf(cells[index] ?? null);
|
|
83
|
+
|
|
72
84
|
if (value !== undefined) {
|
|
73
85
|
values[column.name] = value;
|
|
74
86
|
}
|
|
75
87
|
});
|
|
88
|
+
|
|
76
89
|
return values;
|
|
77
90
|
};
|
|
@@ -17,14 +17,19 @@ export const matchColumns = (fileColumns: readonly string[], tableColumns: reado
|
|
|
17
17
|
if (!header) {
|
|
18
18
|
return fileColumns.map((_, index) => tableColumns[index] ?? null);
|
|
19
19
|
}
|
|
20
|
+
|
|
20
21
|
const taken = new Set<string>();
|
|
22
|
+
|
|
21
23
|
return fileColumns.map((name) => {
|
|
22
24
|
const wanted = name.trim().toLowerCase();
|
|
23
25
|
const match = tableColumns.find((column) => !taken.has(column) && column.toLowerCase() === wanted);
|
|
26
|
+
|
|
24
27
|
if (match === undefined) {
|
|
25
28
|
return null;
|
|
26
29
|
}
|
|
30
|
+
|
|
27
31
|
taken.add(match);
|
|
32
|
+
|
|
28
33
|
return match;
|
|
29
34
|
});
|
|
30
35
|
};
|
|
@@ -35,16 +35,21 @@ export const parseStoredLayout = (text: string | null): StoredLayout | null => {
|
|
|
35
35
|
if (text === null) {
|
|
36
36
|
return null;
|
|
37
37
|
}
|
|
38
|
+
|
|
38
39
|
let data: unknown;
|
|
40
|
+
|
|
39
41
|
try {
|
|
40
42
|
data = JSON.parse(text);
|
|
41
43
|
} catch {
|
|
42
44
|
return null;
|
|
43
45
|
}
|
|
46
|
+
|
|
44
47
|
if (!isRecord(data) || data.version !== LAYOUT_VERSION) {
|
|
45
48
|
return null;
|
|
46
49
|
}
|
|
50
|
+
|
|
47
51
|
const widths: Record<string, number> = {};
|
|
52
|
+
|
|
48
53
|
if (isRecord(data.widths)) {
|
|
49
54
|
for (const [name, width] of Object.entries(data.widths)) {
|
|
50
55
|
if (typeof width === 'number' && Number.isFinite(width) && width > 0) {
|
|
@@ -52,7 +57,9 @@ export const parseStoredLayout = (text: string | null): StoredLayout | null => {
|
|
|
52
57
|
}
|
|
53
58
|
}
|
|
54
59
|
}
|
|
60
|
+
|
|
55
61
|
const pageSize = PAGE_SIZES.find((size) => size === data.pageSize) ?? null;
|
|
62
|
+
|
|
56
63
|
return {
|
|
57
64
|
widths,
|
|
58
65
|
hidden: isStringList(data.hidden) ? data.hidden : [],
|
package/src/table/paging.ts
CHANGED
|
@@ -19,6 +19,7 @@ export interface PageBounds {
|
|
|
19
19
|
export const pageBounds = (offset: number, rowCount: number, hasMore: boolean, counted: number | null): PageBounds => {
|
|
20
20
|
const exact = counted !== null || !hasMore;
|
|
21
21
|
const total = counted ?? (hasMore ? offset + rowCount + 1 : offset + rowCount);
|
|
22
|
+
|
|
22
23
|
return {
|
|
23
24
|
from: rowCount === 0 ? 0 : offset + 1,
|
|
24
25
|
to: offset + rowCount,
|