@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.
- 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/pending.ts
CHANGED
|
@@ -30,9 +30,11 @@ export const valuesEqual = (left: EditValue | undefined, right: EditValue | unde
|
|
|
30
30
|
if (left === undefined || right === undefined) {
|
|
31
31
|
return false;
|
|
32
32
|
}
|
|
33
|
+
|
|
33
34
|
if (left === null || right === null || typeof left !== 'object' || typeof right !== 'object') {
|
|
34
35
|
return left === right;
|
|
35
36
|
}
|
|
37
|
+
|
|
36
38
|
return left.kind === right.kind && (left.kind !== 'binary' || (right.kind === 'binary' && left.hex === right.hex));
|
|
37
39
|
};
|
|
38
40
|
|
|
@@ -41,6 +43,7 @@ export const setEdit = (pending: PendingChanges, row: number, column: string, va
|
|
|
41
43
|
const { [row]: current = {}, ...others } = pending.edits;
|
|
42
44
|
const { [column]: _replaced, ...kept } = current;
|
|
43
45
|
const next = valuesEqual(value, valueOfCell(original)) ? kept : { ...kept, [column]: value };
|
|
46
|
+
|
|
44
47
|
return { ...pending, edits: Object.keys(next).length === 0 ? others : { ...others, [row]: next } };
|
|
45
48
|
};
|
|
46
49
|
|
|
@@ -69,6 +72,7 @@ export const revertRow = (pending: PendingChanges, row: number): PendingChanges
|
|
|
69
72
|
const { [row]: _dropped, ...edits } = pending.edits;
|
|
70
73
|
const deletes = new Set(pending.deletes);
|
|
71
74
|
deletes.delete(row);
|
|
75
|
+
|
|
72
76
|
return { ...pending, edits, deletes };
|
|
73
77
|
};
|
|
74
78
|
|
|
@@ -81,6 +85,7 @@ export interface RowSelectionRefs {
|
|
|
81
85
|
/* Takes back every change to the given rows: the edits and deletion marks of the loaded ones, and the inserted ones altogether. */
|
|
82
86
|
export const revertRows = (pending: PendingChanges, rows: RowSelectionRefs): PendingChanges => {
|
|
83
87
|
const reverted = rows.loaded.reduce(revertRow, pending);
|
|
88
|
+
|
|
84
89
|
return { ...reverted, inserts: reverted.inserts.filter((row) => !rows.inserted.includes(row.id)) };
|
|
85
90
|
};
|
|
86
91
|
|
|
@@ -97,18 +102,24 @@ type LoadedRows = Pick<RowsResult, 'columns' | 'rows'>;
|
|
|
97
102
|
*/
|
|
98
103
|
export const rowKeyOf = (structure: TableStructure, loaded: LoadedRows, row: number): RowKey | null => {
|
|
99
104
|
const cells = loaded.rows[row];
|
|
105
|
+
|
|
100
106
|
if (structure.rowKey === null || cells === undefined) {
|
|
101
107
|
return null;
|
|
102
108
|
}
|
|
109
|
+
|
|
103
110
|
const key: Record<string, Value> = {};
|
|
111
|
+
|
|
104
112
|
for (const name of structure.rowKey) {
|
|
105
113
|
const index = loaded.columns.findIndex((column) => column.name === name);
|
|
106
114
|
const value = index < 0 ? undefined : valueOfCell(cells[index] as Cell);
|
|
115
|
+
|
|
107
116
|
if (value === undefined) {
|
|
108
117
|
return null;
|
|
109
118
|
}
|
|
119
|
+
|
|
110
120
|
key[name] = value;
|
|
111
121
|
}
|
|
122
|
+
|
|
112
123
|
return key;
|
|
113
124
|
};
|
|
114
125
|
|
|
@@ -116,18 +127,23 @@ export const rowKeyOf = (structure: TableStructure, loaded: LoadedRows, row: num
|
|
|
116
127
|
export const toRowChanges = (structure: TableStructure, loaded: LoadedRows, pending: PendingChanges): RowChange[] => {
|
|
117
128
|
const keyOf = (row: number): RowKey => {
|
|
118
129
|
const key = rowKeyOf(structure, loaded, row);
|
|
130
|
+
|
|
119
131
|
if (key === null) {
|
|
120
132
|
throw new Error(`Row ${row + 1} of ${structure.name} has no usable key.`);
|
|
121
133
|
}
|
|
134
|
+
|
|
122
135
|
return key;
|
|
123
136
|
};
|
|
137
|
+
|
|
124
138
|
const updates = Object.keys(pending.edits)
|
|
125
139
|
.map(Number)
|
|
126
140
|
.filter((row) => !pending.deletes.has(row))
|
|
127
141
|
.sort((left, right) => left - right)
|
|
128
142
|
.map((row): RowChange => ({ kind: 'update', key: keyOf(row), values: pending.edits[row]! }));
|
|
143
|
+
|
|
129
144
|
const deletes = [...pending.deletes].sort((left, right) => left - right).map((row): RowChange => ({ kind: 'delete', key: keyOf(row) }));
|
|
130
145
|
const inserts = pending.inserts.map((row): RowChange => ({ kind: 'insert', values: row.values }));
|
|
146
|
+
|
|
131
147
|
return [...updates, ...deletes, ...inserts];
|
|
132
148
|
};
|
|
133
149
|
|
|
@@ -135,9 +151,11 @@ const keyValueText = (value: Value): string => {
|
|
|
135
151
|
if (value === null) {
|
|
136
152
|
return 'NULL';
|
|
137
153
|
}
|
|
154
|
+
|
|
138
155
|
if (typeof value === 'object') {
|
|
139
156
|
return `0x${value.hex}`;
|
|
140
157
|
}
|
|
158
|
+
|
|
141
159
|
return typeof value === 'string' ? `'${value}'` : String(value);
|
|
142
160
|
};
|
|
143
161
|
|
package/src/table/references.ts
CHANGED
|
@@ -25,14 +25,18 @@ export const referenceOf = (
|
|
|
25
25
|
cells: readonly Shown[]
|
|
26
26
|
): Reference | null => {
|
|
27
27
|
const matches: ColumnMatch[] = [];
|
|
28
|
+
|
|
28
29
|
for (const [position, name] of key.columns.entries()) {
|
|
29
30
|
const referenced = key.referencedColumns[position];
|
|
30
31
|
const index = columns.findIndex((column) => column.name === name);
|
|
31
32
|
const value = index < 0 ? undefined : wholeValueOf(cells[index] ?? null);
|
|
33
|
+
|
|
32
34
|
if (referenced === undefined || value === undefined || value === null) {
|
|
33
35
|
return null;
|
|
34
36
|
}
|
|
37
|
+
|
|
35
38
|
matches.push({ column: referenced, value, kind: columns[index]!.kind });
|
|
36
39
|
}
|
|
40
|
+
|
|
37
41
|
return matches.length === 0 ? null : { schema: key.referencedSchema, table: key.referencedTable, where: matchCondition(engine, matches) };
|
|
38
42
|
};
|
package/src/table/useLoaded.ts
CHANGED
|
@@ -33,6 +33,7 @@ export function useLoaded<T>(load: (signal: AbortSignal) => Promise<T>, key: str
|
|
|
33
33
|
|
|
34
34
|
useEffect(() => {
|
|
35
35
|
const controller = new AbortController();
|
|
36
|
+
|
|
36
37
|
loader.current(controller.signal).then(
|
|
37
38
|
(value) => {
|
|
38
39
|
if (!controller.signal.aborted) {
|
|
@@ -45,10 +46,12 @@ export function useLoaded<T>(load: (signal: AbortSignal) => Promise<T>, key: str
|
|
|
45
46
|
}
|
|
46
47
|
}
|
|
47
48
|
);
|
|
49
|
+
|
|
48
50
|
return () => controller.abort();
|
|
49
51
|
}, [key, version]);
|
|
50
52
|
|
|
51
53
|
const answered = state.key === key && state.version === version;
|
|
54
|
+
|
|
52
55
|
return {
|
|
53
56
|
value: state.value,
|
|
54
57
|
loading: !answered,
|
|
@@ -30,6 +30,7 @@ export function useStoredLayout(storage: DatabaseStorage | undefined, key: strin
|
|
|
30
30
|
const write = useCallback((): void => {
|
|
31
31
|
clearTimeout(timer.current);
|
|
32
32
|
timer.current = undefined;
|
|
33
|
+
|
|
33
34
|
if (dirty.current) {
|
|
34
35
|
dirty.current = false;
|
|
35
36
|
target.current.storage?.set(target.current.key, serializeLayout(latest.current));
|
|
@@ -44,6 +45,7 @@ export function useStoredLayout(storage: DatabaseStorage | undefined, key: strin
|
|
|
44
45
|
if (storage === undefined) {
|
|
45
46
|
return;
|
|
46
47
|
}
|
|
48
|
+
|
|
47
49
|
latest.current = { ...latest.current, ...change };
|
|
48
50
|
dirty.current = true;
|
|
49
51
|
clearTimeout(timer.current);
|
|
@@ -49,7 +49,6 @@ export interface TableTransfer {
|
|
|
49
49
|
readonly notice: TransferNotice | null;
|
|
50
50
|
readonly importDraft: ImportDraft | null;
|
|
51
51
|
exportAs(format: FileFormat): void;
|
|
52
|
-
/* Stops the export that is running. */
|
|
53
52
|
cancelExport(): void;
|
|
54
53
|
dismiss(): void;
|
|
55
54
|
chooseImportFile(): void;
|
|
@@ -71,7 +70,9 @@ export function useTableTransfer({ client, session, files, schema, table, struct
|
|
|
71
70
|
if (notice?.kind !== 'exported' && notice?.kind !== 'imported') {
|
|
72
71
|
return;
|
|
73
72
|
}
|
|
73
|
+
|
|
74
74
|
const timer = setTimeout(() => setNotice(null), NOTICE_MS);
|
|
75
|
+
|
|
75
76
|
return () => clearTimeout(timer);
|
|
76
77
|
}, [notice]);
|
|
77
78
|
|
|
@@ -90,19 +91,24 @@ export function useTableTransfer({ client, session, files, schema, table, struct
|
|
|
90
91
|
if (files === undefined || exporting.current !== null) {
|
|
91
92
|
return;
|
|
92
93
|
}
|
|
94
|
+
|
|
93
95
|
void (async () => {
|
|
94
96
|
const path = await files.save({ suggestedName: `${table}.${format}`, format });
|
|
97
|
+
|
|
95
98
|
if (path === null) {
|
|
96
99
|
return;
|
|
97
100
|
}
|
|
101
|
+
|
|
98
102
|
const controller = new AbortController();
|
|
99
103
|
exporting.current = controller;
|
|
100
104
|
setNotice({ kind: 'exporting', format });
|
|
105
|
+
|
|
101
106
|
try {
|
|
102
107
|
const result = await session.export(
|
|
103
108
|
{ source: { kind: 'table', schema, table, where: where || undefined, orderBy: orderBy || undefined }, format, path },
|
|
104
109
|
{ signal: controller.signal }
|
|
105
110
|
);
|
|
111
|
+
|
|
106
112
|
show({ kind: 'exported', rows: result.rows });
|
|
107
113
|
} catch (error) {
|
|
108
114
|
if (controller.signal.aborted || isCancelled(error)) {
|
|
@@ -120,6 +126,7 @@ export function useTableTransfer({ client, session, files, schema, table, struct
|
|
|
120
126
|
sampling.current?.abort();
|
|
121
127
|
const controller = new AbortController();
|
|
122
128
|
sampling.current = controller;
|
|
129
|
+
|
|
123
130
|
return await client.sample(path, format, header, { signal: controller.signal });
|
|
124
131
|
};
|
|
125
132
|
|
|
@@ -127,12 +134,16 @@ export function useTableTransfer({ client, session, files, schema, table, struct
|
|
|
127
134
|
if (files === undefined || structure === null) {
|
|
128
135
|
return;
|
|
129
136
|
}
|
|
137
|
+
|
|
130
138
|
void (async () => {
|
|
131
139
|
const path = await files.open({ formats: ['csv', 'tsv'] });
|
|
140
|
+
|
|
132
141
|
if (path === null) {
|
|
133
142
|
return;
|
|
134
143
|
}
|
|
144
|
+
|
|
135
145
|
const format = formatOfPath(path);
|
|
146
|
+
|
|
136
147
|
try {
|
|
137
148
|
const read = await sample(path, format, true);
|
|
138
149
|
setImportDraft({ path, format, header: true, sample: read, mapping: matchColumns(read.columns, tableColumns, true), busy: false, error: null });
|
|
@@ -146,11 +157,14 @@ export function useTableTransfer({ client, session, files, schema, table, struct
|
|
|
146
157
|
if (importDraft === null) {
|
|
147
158
|
return;
|
|
148
159
|
}
|
|
160
|
+
|
|
149
161
|
const { path, format } = importDraft;
|
|
150
162
|
setImportDraft({ ...importDraft, header, sample: null, error: null });
|
|
163
|
+
|
|
151
164
|
void (async () => {
|
|
152
165
|
try {
|
|
153
166
|
const read = await sample(path, format, header);
|
|
167
|
+
|
|
154
168
|
setImportDraft((now) =>
|
|
155
169
|
now === null || now.path !== path ? now : { ...now, header, sample: read, mapping: matchColumns(read.columns, tableColumns, header) }
|
|
156
170
|
);
|
|
@@ -170,8 +184,10 @@ export function useTableTransfer({ client, session, files, schema, table, struct
|
|
|
170
184
|
if (importDraft === null || importDraft.busy || importDraft.sample === null) {
|
|
171
185
|
return;
|
|
172
186
|
}
|
|
187
|
+
|
|
173
188
|
const { path, format, header, mapping } = importDraft;
|
|
174
189
|
setImportDraft({ ...importDraft, busy: true, error: null });
|
|
190
|
+
|
|
175
191
|
void (async () => {
|
|
176
192
|
try {
|
|
177
193
|
const rows = await session.import(schema, table, { path, format, header, columns: mapping });
|
package/src/testing/fake.ts
CHANGED
|
@@ -149,6 +149,7 @@ const rowKeyOf = (table: Table): string[] | null => {
|
|
|
149
149
|
const unique = table.indexes.find(
|
|
150
150
|
(index) => index.unique && index.columns.every((name) => table.columns.find((column) => column.name === name)?.nullable === false)
|
|
151
151
|
);
|
|
152
|
+
|
|
152
153
|
return unique === undefined ? null : [...unique.columns];
|
|
153
154
|
};
|
|
154
155
|
|
|
@@ -180,6 +181,7 @@ const toCell = (value: Value, cellLimit: number): Cell => {
|
|
|
180
181
|
|
|
181
182
|
if (value !== null && typeof value === 'object') {
|
|
182
183
|
const length = value.hex.length / 2;
|
|
184
|
+
|
|
183
185
|
return { kind: 'binary', hex: value.hex.slice(0, Math.min(length, cellLimit) * 2), length };
|
|
184
186
|
}
|
|
185
187
|
|
|
@@ -196,16 +198,20 @@ interface ByteBudget {
|
|
|
196
198
|
|
|
197
199
|
const toRowsResult = (table: Table, rows: readonly (readonly Value[])[], offset: number, limit: number, cellLimit: number, budget: ByteBudget): RowsResult => {
|
|
198
200
|
const page: Cell[][] = [];
|
|
201
|
+
|
|
199
202
|
for (const row of rows.slice(offset, offset + limit)) {
|
|
200
203
|
const cells = row.map((value) => toCell(value, cellLimit));
|
|
201
204
|
// Characters rather than bytes: near enough for a fake.
|
|
202
205
|
const size = JSON.stringify(cells).length;
|
|
206
|
+
|
|
203
207
|
if (page.length > 0 && size > budget.left) {
|
|
204
208
|
return { columns: toResultColumns(table), rows: page, hasMore: true, elapsedMs: 0 };
|
|
205
209
|
}
|
|
210
|
+
|
|
206
211
|
budget.left = Math.max(0, budget.left - size);
|
|
207
212
|
page.push(cells);
|
|
208
213
|
}
|
|
214
|
+
|
|
209
215
|
return { columns: toResultColumns(table), rows: page, hasMore: rows.length > offset + limit, elapsedMs: 0 };
|
|
210
216
|
};
|
|
211
217
|
|
|
@@ -213,6 +219,7 @@ const toRowsResult = (table: Table, rows: readonly (readonly Value[])[], offset:
|
|
|
213
219
|
const defaultOf = (column: ColumnInfo, table: Table): Value => {
|
|
214
220
|
if (column.autoIncrement) {
|
|
215
221
|
const index = table.columns.indexOf(column);
|
|
222
|
+
|
|
216
223
|
return table.rows.reduce((highest, row) => (typeof row[index] === 'number' ? Math.max(highest, row[index]) : highest), 0) + 1;
|
|
217
224
|
}
|
|
218
225
|
|
|
@@ -272,6 +279,7 @@ const applyChanges = (table: Table, changes: DatabaseParams<'apply'>['changes'])
|
|
|
272
279
|
|
|
273
280
|
const row = table.columns.map((column) => {
|
|
274
281
|
const given = Object.hasOwn(change.values, column.name) ? change.values[column.name]! : { kind: 'default' as const };
|
|
282
|
+
|
|
275
283
|
return resolveEdit(column, draft, given);
|
|
276
284
|
});
|
|
277
285
|
|
|
@@ -285,6 +293,7 @@ const applyChanges = (table: Table, changes: DatabaseParams<'apply'>['changes'])
|
|
|
285
293
|
}
|
|
286
294
|
|
|
287
295
|
draft.rows.push(row);
|
|
296
|
+
|
|
288
297
|
return;
|
|
289
298
|
}
|
|
290
299
|
|
|
@@ -302,6 +311,7 @@ const applyChanges = (table: Table, changes: DatabaseParams<'apply'>['changes'])
|
|
|
302
311
|
|
|
303
312
|
if (change.kind === 'delete') {
|
|
304
313
|
draft.rows.splice(draft.rows.indexOf(row), 1);
|
|
314
|
+
|
|
305
315
|
return;
|
|
306
316
|
}
|
|
307
317
|
|
|
@@ -312,6 +322,7 @@ const applyChanges = (table: Table, changes: DatabaseParams<'apply'>['changes'])
|
|
|
312
322
|
});
|
|
313
323
|
|
|
314
324
|
table.rows = draft.rows;
|
|
325
|
+
|
|
315
326
|
return changes.length;
|
|
316
327
|
};
|
|
317
328
|
|
|
@@ -331,6 +342,7 @@ const selectedTable = (session: Session, sql: string): Table => {
|
|
|
331
342
|
const runStatement = (session: Session, sql: string, limit: number, cellLimit: number, budget: ByteBudget): StatementResult => {
|
|
332
343
|
try {
|
|
333
344
|
const table = selectedTable(session, sql);
|
|
345
|
+
|
|
334
346
|
return { kind: 'rows', sql, ...toRowsResult(table, table.rows, 0, limit, cellLimit, budget) };
|
|
335
347
|
} catch (error) {
|
|
336
348
|
if (error instanceof FakeError) {
|
|
@@ -402,15 +414,18 @@ export const fakeDatabaseTransport = (options: FakeDatabaseTransportOptions): Da
|
|
|
402
414
|
const session = connect(connection);
|
|
403
415
|
const id = `fake-${++counter}`;
|
|
404
416
|
sessions.set(id, session);
|
|
417
|
+
|
|
405
418
|
return { session: id, server: serverOf(connection) };
|
|
406
419
|
},
|
|
407
420
|
close: ({ session }) => {
|
|
408
421
|
sessionOf(session);
|
|
409
422
|
sessions.delete(session);
|
|
423
|
+
|
|
410
424
|
return null;
|
|
411
425
|
},
|
|
412
426
|
test: ({ connection }) => {
|
|
413
427
|
connect(connection);
|
|
428
|
+
|
|
414
429
|
return { server: serverOf(connection) };
|
|
415
430
|
},
|
|
416
431
|
schemas: ({ session }) => ({
|
|
@@ -418,10 +433,12 @@ export const fakeDatabaseTransport = (options: FakeDatabaseTransportOptions): Da
|
|
|
418
433
|
}),
|
|
419
434
|
tables: ({ session, schema }) => {
|
|
420
435
|
const tables = findSchema(sessionOf(session), schema);
|
|
436
|
+
|
|
421
437
|
return { tables: [...tables].map(([name, table]) => ({ name, kind: table.kind, rowEstimate: table.rows.length, comment: null })) };
|
|
422
438
|
},
|
|
423
439
|
structure: ({ session, schema, table: name }) => {
|
|
424
440
|
const table = findTable(sessionOf(session), schema, name);
|
|
441
|
+
|
|
425
442
|
return {
|
|
426
443
|
schema,
|
|
427
444
|
name,
|
|
@@ -438,6 +455,7 @@ export const fakeDatabaseTransport = (options: FakeDatabaseTransportOptions): Da
|
|
|
438
455
|
},
|
|
439
456
|
rows: ({ session, schema, table: name, offset, limit, cellLimit, maxBytes }) => {
|
|
440
457
|
const table = findTable(sessionOf(session), schema, name);
|
|
458
|
+
|
|
441
459
|
return toRowsResult(table, table.rows, offset, limit, cellLimit ?? DEFAULT_ROWS_CELL_LIMIT, { left: maxBytes ?? DEFAULT_MAX_BYTES });
|
|
442
460
|
},
|
|
443
461
|
count: ({ session, schema, table: name }) => ({ count: findTable(sessionOf(session), schema, name).rows.length }),
|
|
@@ -491,15 +509,18 @@ export const fakeDatabaseTransport = (options: FakeDatabaseTransportOptions): Da
|
|
|
491
509
|
const target = sessionOf(session);
|
|
492
510
|
selectSchema(target, schema);
|
|
493
511
|
const table = selectedTable(target, sql);
|
|
512
|
+
|
|
494
513
|
return toRowsResult(table, table.rows, offset, limit, cellLimit ?? DEFAULT_EXECUTE_CELL_LIMIT, { left: maxBytes ?? DEFAULT_MAX_BYTES });
|
|
495
514
|
},
|
|
496
515
|
transaction: ({ session, action }) => {
|
|
497
516
|
const target = sessionOf(session);
|
|
498
517
|
target.inTransaction = action === 'begin';
|
|
518
|
+
|
|
499
519
|
return { active: target.inTransaction };
|
|
500
520
|
},
|
|
501
521
|
export: ({ session }) => {
|
|
502
522
|
sessionOf(session);
|
|
523
|
+
|
|
503
524
|
throw new FakeError('unsupported', 'The fake database has no files to export to.');
|
|
504
525
|
},
|
|
505
526
|
sample: () => {
|
|
@@ -507,12 +528,14 @@ export const fakeDatabaseTransport = (options: FakeDatabaseTransportOptions): Da
|
|
|
507
528
|
},
|
|
508
529
|
import: ({ session }) => {
|
|
509
530
|
sessionOf(session);
|
|
531
|
+
|
|
510
532
|
throw new FakeError('unsupported', 'The fake database has no files to import from.');
|
|
511
533
|
},
|
|
512
534
|
discover: () => ({ containers: structuredClone([...(options.containers ?? [])]) }),
|
|
513
535
|
cancel: ({ request }) => {
|
|
514
536
|
const stop = waiting.get(request);
|
|
515
537
|
stop?.();
|
|
538
|
+
|
|
516
539
|
return { cancelled: stop !== undefined };
|
|
517
540
|
}
|
|
518
541
|
};
|
|
@@ -520,6 +543,7 @@ export const fakeDatabaseTransport = (options: FakeDatabaseTransportOptions): Da
|
|
|
520
543
|
const answer = (request: DatabaseRequest): DatabaseResponse => {
|
|
521
544
|
try {
|
|
522
545
|
const handler = handlers[request.method] as (params: unknown) => unknown;
|
|
546
|
+
|
|
523
547
|
return { id: request.id, ok: true, result: handler(request.params) } as DatabaseResponse;
|
|
524
548
|
} catch (error) {
|
|
525
549
|
if (error instanceof FakeError) {
|
|
@@ -7,6 +7,7 @@ import { addDatabaseResources } from './locales.ts';
|
|
|
7
7
|
export function useDatabaseResources(): void {
|
|
8
8
|
const { i18n } = useTranslation();
|
|
9
9
|
const [added, setAdded] = useState<I18n | null>(null);
|
|
10
|
+
|
|
10
11
|
if (added !== i18n) {
|
|
11
12
|
addDatabaseResources(i18n);
|
|
12
13
|
setAdded(i18n);
|
package/src/use-popup-press.ts
CHANGED
|
@@ -13,7 +13,9 @@ export function usePopupPress(): () => boolean {
|
|
|
13
13
|
const press = (e: PointerEvent): void => {
|
|
14
14
|
pressed.current = isInFloatingLayer(e.target);
|
|
15
15
|
};
|
|
16
|
+
|
|
16
17
|
document.addEventListener('pointerdown', press, true);
|
|
18
|
+
|
|
17
19
|
return () => {
|
|
18
20
|
document.removeEventListener('pointerdown', press, true);
|
|
19
21
|
};
|
|
@@ -22,6 +24,7 @@ export function usePopupPress(): () => boolean {
|
|
|
22
24
|
return () => {
|
|
23
25
|
const was = pressed.current;
|
|
24
26
|
pressed.current = false;
|
|
27
|
+
|
|
25
28
|
return was;
|
|
26
29
|
};
|
|
27
30
|
}
|
|
@@ -75,7 +75,9 @@ export function ValueEditor({ column, value, editable, onCommit }: ValueEditorPr
|
|
|
75
75
|
if (!editable || onCommit === undefined || !dirty) {
|
|
76
76
|
return;
|
|
77
77
|
}
|
|
78
|
+
|
|
78
79
|
const conversion = editValueOf(draft, source);
|
|
80
|
+
|
|
79
81
|
if (conversion.ok) {
|
|
80
82
|
onCommit(conversion.value);
|
|
81
83
|
} else {
|
|
@@ -123,9 +125,11 @@ export function ValueEditor({ column, value, editable, onCommit }: ValueEditorPr
|
|
|
123
125
|
if (content.kind === 'empty') {
|
|
124
126
|
return <Faint>{placeholder}</Faint>;
|
|
125
127
|
}
|
|
128
|
+
|
|
126
129
|
if (content.kind === 'problem') {
|
|
127
130
|
return <Faint>{problemText(content.problem)}</Faint>;
|
|
128
131
|
}
|
|
132
|
+
|
|
129
133
|
return (
|
|
130
134
|
<>
|
|
131
135
|
<pre className={clsx(READING, wrapped ? 'break-words whitespace-pre-wrap' : 'whitespace-pre')}>{content.text}</pre>
|
|
@@ -142,6 +146,7 @@ export function ValueEditor({ column, value, editable, onCommit }: ValueEditorPr
|
|
|
142
146
|
if (view === 'text') {
|
|
143
147
|
return editor('min-h-0 flex-1');
|
|
144
148
|
}
|
|
149
|
+
|
|
145
150
|
if (view === 'hex' && editable) {
|
|
146
151
|
return (
|
|
147
152
|
<>
|
|
@@ -150,6 +155,7 @@ export function ValueEditor({ column, value, editable, onCommit }: ValueEditorPr
|
|
|
150
155
|
</>
|
|
151
156
|
);
|
|
152
157
|
}
|
|
158
|
+
|
|
153
159
|
return rendered === null ? null : reading(rendered, wrap && WRAPPING_VIEWS.has(view));
|
|
154
160
|
};
|
|
155
161
|
|
package/src/value/draft.ts
CHANGED
|
@@ -30,6 +30,7 @@ const textOf = (value: Exclude<Value, null>): string => (typeof value === 'objec
|
|
|
30
30
|
export const sourceOf = (column: Pick<GridColumn, 'name' | 'type' | 'kind'>, value: Value): Source => {
|
|
31
31
|
const binary = column.kind === 'binary' || (value !== null && typeof value === 'object');
|
|
32
32
|
const original: Draft = value === null ? { text: '', mode: 'null' } : { text: textOf(value), mode: 'value' };
|
|
33
|
+
|
|
33
34
|
return {
|
|
34
35
|
name: column.name,
|
|
35
36
|
type: column.type,
|
|
@@ -50,18 +51,24 @@ export const editValueOf = (draft: Draft, source: Source): Conversion => {
|
|
|
50
51
|
if (draft.mode === 'null') {
|
|
51
52
|
return { ok: true, value: null };
|
|
52
53
|
}
|
|
54
|
+
|
|
53
55
|
if (draft.mode === 'default') {
|
|
54
56
|
return { ok: true, value: { kind: 'default' } };
|
|
55
57
|
}
|
|
58
|
+
|
|
56
59
|
if (source.binary) {
|
|
57
60
|
const parsed = parseHex(draft.text);
|
|
61
|
+
|
|
58
62
|
return parsed.ok ? { ok: true, value: { kind: 'binary', hex: parsed.hex } } : { ok: false, problem: { kind: 'hex', problem: parsed.problem } };
|
|
59
63
|
}
|
|
64
|
+
|
|
60
65
|
if (source.json) {
|
|
61
66
|
const checked = checkJson(draft.text);
|
|
67
|
+
|
|
62
68
|
if (!checked.ok) {
|
|
63
69
|
return { ok: false, problem: { kind: 'json', reason: checked.reason } };
|
|
64
70
|
}
|
|
65
71
|
}
|
|
72
|
+
|
|
66
73
|
return { ok: true, value: parseDraft(draft.text, source.kind) };
|
|
67
74
|
};
|
package/src/value/hex.ts
CHANGED
|
@@ -15,17 +15,21 @@ const UUID_BYTES = 16;
|
|
|
15
15
|
export const parseHex = (text: string): ParsedHex => {
|
|
16
16
|
const stripped = text.replace(/\s+/g, '').toLowerCase();
|
|
17
17
|
const hex = stripped.startsWith('0x') ? stripped.slice(2) : stripped;
|
|
18
|
+
|
|
18
19
|
if (NOT_HEX.test(hex)) {
|
|
19
20
|
return { ok: false, problem: 'characters' };
|
|
20
21
|
}
|
|
22
|
+
|
|
21
23
|
return hex.length % 2 === 0 ? { ok: true, hex } : { ok: false, problem: 'odd' };
|
|
22
24
|
};
|
|
23
25
|
|
|
24
26
|
export const bytesOf = (hex: string): Uint8Array => {
|
|
25
27
|
const bytes = new Uint8Array(hex.length / 2);
|
|
28
|
+
|
|
26
29
|
for (let i = 0; i < bytes.length; i++) {
|
|
27
30
|
bytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
28
31
|
}
|
|
32
|
+
|
|
29
33
|
return bytes;
|
|
30
34
|
};
|
|
31
35
|
|
|
@@ -37,6 +41,7 @@ export const formatUuid = (hex: string): string | null => {
|
|
|
37
41
|
if (hex.length !== UUID_BYTES * 2) {
|
|
38
42
|
return null;
|
|
39
43
|
}
|
|
44
|
+
|
|
40
45
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
41
46
|
};
|
|
42
47
|
|
|
@@ -51,12 +56,15 @@ const slotsOf = (bytes: Uint8Array, from: number, to: number): string =>
|
|
|
51
56
|
export const hexDumpLines = (hex: string, maxBytes = Number.POSITIVE_INFINITY): string[] => {
|
|
52
57
|
const bytes = bytesOf(hex.slice(0, Math.min(hex.length, maxBytes * 2)));
|
|
53
58
|
const lines: string[] = [];
|
|
59
|
+
|
|
54
60
|
for (let offset = 0; offset < bytes.length; offset += BYTES_PER_LINE) {
|
|
55
61
|
const line = bytes.subarray(offset, offset + BYTES_PER_LINE);
|
|
56
62
|
const ascii = Array.from(line, (byte) => (isPrintable(byte) ? String.fromCharCode(byte) : '.')).join('');
|
|
63
|
+
|
|
57
64
|
lines.push(
|
|
58
65
|
`${offset.toString(16).padStart(8, '0')} ${slotsOf(bytes, offset, offset + GROUP)} ${slotsOf(bytes, offset + GROUP, offset + BYTES_PER_LINE)} |${ascii}|`
|
|
59
66
|
);
|
|
60
67
|
}
|
|
68
|
+
|
|
61
69
|
return lines;
|
|
62
70
|
};
|
package/src/value/json.ts
CHANGED
|
@@ -7,6 +7,7 @@ export type JsonCheck = { readonly ok: true } | { readonly ok: false; readonly r
|
|
|
7
7
|
export const checkJson = (text: string): JsonCheck => {
|
|
8
8
|
try {
|
|
9
9
|
JSON.parse(text);
|
|
10
|
+
|
|
10
11
|
return { ok: true };
|
|
11
12
|
} catch (error) {
|
|
12
13
|
return { ok: false, reason: error instanceof Error ? error.message : String(error) };
|
|
@@ -16,6 +17,7 @@ export const checkJson = (text: string): JsonCheck => {
|
|
|
16
17
|
/* Whether a string is a JSON object or array. A scalar such as `42` or `"x"` is a plain value, not a document. */
|
|
17
18
|
export const isJsonDocument = (text: string): boolean => {
|
|
18
19
|
const start = text.trimStart()[0];
|
|
20
|
+
|
|
19
21
|
return (start === '{' || start === '[') && checkJson(text).ok;
|
|
20
22
|
};
|
|
21
23
|
|
|
@@ -28,34 +30,46 @@ export const prettyJson = (text: string): string | null => {
|
|
|
28
30
|
let output = '';
|
|
29
31
|
let depth = 0;
|
|
30
32
|
let i = 0;
|
|
33
|
+
|
|
31
34
|
const lineBreak = () => `\n${INDENT.repeat(depth)}`;
|
|
35
|
+
|
|
32
36
|
const nextToken = (from: number): string => {
|
|
33
37
|
let at = from;
|
|
38
|
+
|
|
34
39
|
while (/\s/.test(text[at] ?? '')) {
|
|
35
40
|
at++;
|
|
36
41
|
}
|
|
42
|
+
|
|
37
43
|
return text[at] ?? '';
|
|
38
44
|
};
|
|
39
45
|
|
|
40
46
|
while (i < text.length) {
|
|
41
47
|
const char = text[i]!;
|
|
48
|
+
|
|
42
49
|
if (char === '"') {
|
|
43
50
|
let end = i + 1;
|
|
51
|
+
|
|
44
52
|
while (text[end] !== '"') {
|
|
45
53
|
end += text[end] === '\\' ? 2 : 1;
|
|
46
54
|
}
|
|
55
|
+
|
|
47
56
|
output += text.slice(i, end + 1);
|
|
48
57
|
i = end + 1;
|
|
58
|
+
|
|
49
59
|
continue;
|
|
50
60
|
}
|
|
61
|
+
|
|
51
62
|
if (char === '{' || char === '[') {
|
|
52
63
|
const close = char === '{' ? '}' : ']';
|
|
53
64
|
output += char;
|
|
65
|
+
|
|
54
66
|
if (nextToken(i + 1) === close) {
|
|
55
67
|
output += close;
|
|
56
68
|
i = text.indexOf(close, i + 1) + 1;
|
|
69
|
+
|
|
57
70
|
continue;
|
|
58
71
|
}
|
|
72
|
+
|
|
59
73
|
depth++;
|
|
60
74
|
output += lineBreak();
|
|
61
75
|
} else if (char === '}' || char === ']') {
|
|
@@ -68,7 +82,9 @@ export const prettyJson = (text: string): string | null => {
|
|
|
68
82
|
} else if (!/\s/.test(char)) {
|
|
69
83
|
output += char;
|
|
70
84
|
}
|
|
85
|
+
|
|
71
86
|
i++;
|
|
72
87
|
}
|
|
88
|
+
|
|
73
89
|
return output.trim();
|
|
74
90
|
};
|
package/src/value/views.ts
CHANGED
|
@@ -9,8 +9,10 @@ export const viewsOf = (source: Source, draft: Draft): ViewId[] => {
|
|
|
9
9
|
if (source.binary) {
|
|
10
10
|
const parsed = parseHex(draft.text);
|
|
11
11
|
const hex = parsed.ok ? parsed.hex : source.original.text;
|
|
12
|
+
|
|
12
13
|
return formatUuid(hex) === null ? ['hex', 'utf8'] : ['hex', 'utf8', 'uuid'];
|
|
13
14
|
}
|
|
15
|
+
|
|
14
16
|
return source.json ? ['formatted', 'text'] : ['text'];
|
|
15
17
|
};
|
|
16
18
|
|
|
@@ -19,10 +21,13 @@ export const sizeOf = (source: Source, draft: Draft): { unit: 'bytes' | 'charact
|
|
|
19
21
|
if (draft.mode !== 'value') {
|
|
20
22
|
return null;
|
|
21
23
|
}
|
|
24
|
+
|
|
22
25
|
if (!source.binary) {
|
|
23
26
|
return { unit: 'characters', count: draft.text.length };
|
|
24
27
|
}
|
|
28
|
+
|
|
25
29
|
const parsed = parseHex(draft.text);
|
|
30
|
+
|
|
26
31
|
return parsed.ok ? { unit: 'bytes', count: parsed.hex.length / 2 } : null;
|
|
27
32
|
};
|
|
28
33
|
|
|
@@ -40,26 +45,35 @@ export const renderView = (view: Exclude<ViewId, 'text'>, draft: Draft): Rendere
|
|
|
40
45
|
if (draft.mode !== 'value') {
|
|
41
46
|
return { kind: 'empty' };
|
|
42
47
|
}
|
|
48
|
+
|
|
43
49
|
if (view === 'formatted') {
|
|
44
50
|
const pretty = prettyJson(draft.text);
|
|
51
|
+
|
|
45
52
|
if (pretty !== null) {
|
|
46
53
|
return { kind: 'text', text: pretty };
|
|
47
54
|
}
|
|
55
|
+
|
|
48
56
|
const checked = checkJson(draft.text);
|
|
57
|
+
|
|
49
58
|
return { kind: 'problem', problem: { kind: 'json', reason: checked.ok ? '' : checked.reason } };
|
|
50
59
|
}
|
|
51
60
|
|
|
52
61
|
const parsed = parseHex(draft.text);
|
|
62
|
+
|
|
53
63
|
if (!parsed.ok) {
|
|
54
64
|
return { kind: 'problem', problem: { kind: 'hex', problem: parsed.problem } };
|
|
55
65
|
}
|
|
66
|
+
|
|
56
67
|
if (view === 'utf8') {
|
|
57
68
|
return { kind: 'text', text: decodeUtf8(parsed.hex) };
|
|
58
69
|
}
|
|
70
|
+
|
|
59
71
|
if (view === 'uuid') {
|
|
60
72
|
return { kind: 'text', text: formatUuid(parsed.hex) ?? '' };
|
|
61
73
|
}
|
|
74
|
+
|
|
62
75
|
const total = parsed.hex.length / 2;
|
|
76
|
+
|
|
63
77
|
return {
|
|
64
78
|
kind: 'text',
|
|
65
79
|
text: hexDumpLines(parsed.hex, DUMP_LIMIT_BYTES).join('\n'),
|