@rozenite/sqlite-plugin 1.13.0 → 2.1.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/CHANGELOG.md +23 -0
- package/dist/devtools/assets/panel-BarPLQ4-.css +1 -0
- package/dist/devtools/assets/{panel-DjRs5NTl.js → panel-BsdCagVv.js} +45 -44
- package/dist/devtools/panel.html +2 -2
- package/dist/react-native/chunks/index.require.js +4 -16
- package/dist/react-native/chunks/sql.require.js +1 -3
- package/dist/react-native/chunks/useRozeniteSqlitePlugin.require.js +92 -110
- package/dist/react-native/index.d.ts +2 -2
- package/dist/rozenite.json +1 -1
- package/package.json +27 -31
- package/react-native.ts +7 -14
- package/src/react-native/adapters/__tests__/expo-sqlite.test.ts +4 -14
- package/src/react-native/adapters/expo-sqlite.ts +11 -40
- package/src/react-native/adapters/generic.ts +5 -13
- package/src/react-native/adapters/index.ts +1 -4
- package/src/react-native/sqlite-view.ts +2 -8
- package/src/react-native/useRozeniteSqlitePlugin.ts +110 -130
- package/src/react-native/useSqliteAgentTools.ts +5 -14
- package/src/shared/__tests__/bridge-values.test.ts +6 -11
- package/src/shared/__tests__/sql.test.ts +4 -8
- package/src/shared/bridge-values.ts +2 -5
- package/src/shared/sql.ts +3 -7
- package/src/ui/__tests__/sql-editor-utils.test.ts +1 -5
- package/src/ui/__tests__/sqlite-drop-mutations.test.ts +138 -0
- package/src/ui/__tests__/sqlite-row-mutations.test.ts +1 -3
- package/src/ui/__tests__/sqlite-table-column-order.test.ts +4 -19
- package/src/ui/cell-detail-drawer.tsx +1 -6
- package/src/ui/globals.css +26 -65
- package/src/ui/panel.tsx +442 -568
- package/src/ui/query-result-table.tsx +8 -26
- package/src/ui/sql-editor-utils.ts +24 -55
- package/src/ui/sql-editor.tsx +13 -46
- package/src/ui/sqlite-data-table.tsx +11 -28
- package/src/ui/sqlite-drop-modal.tsx +193 -0
- package/src/ui/sqlite-drop-mutations.ts +53 -0
- package/src/ui/sqlite-introspection.ts +9 -25
- package/src/ui/sqlite-row-delete-modal.tsx +5 -12
- package/src/ui/sqlite-row-edit-modal.tsx +22 -64
- package/src/ui/sqlite-row-edit-value.ts +2 -11
- package/src/ui/sqlite-row-mutations.ts +10 -32
- package/src/ui/sqlite-table-column-order.ts +8 -23
- package/src/ui/use-sqlite-requests.ts +46 -86
- package/src/ui/utils.ts +1 -5
- package/src/ui/value-utils.tsx +3 -11
- package/dist/devtools/assets/panel-CIU0JBOs.css +0 -1
- package/postcss.config.js +0 -6
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
SqliteAdapter,
|
|
3
|
-
SqliteDatabaseInfo,
|
|
4
|
-
SqliteStatementInput,
|
|
5
|
-
} from '../shared/types';
|
|
1
|
+
import type { SqliteAdapter, SqliteDatabaseInfo, SqliteStatementInput } from '../shared/types';
|
|
6
2
|
|
|
7
3
|
export type SqliteDatabaseView = SqliteDatabaseInfo & {
|
|
8
4
|
executeStatements: (
|
|
@@ -10,9 +6,7 @@ export type SqliteDatabaseView = SqliteDatabaseInfo & {
|
|
|
10
6
|
) => ReturnType<SqliteAdapter['databases'][number]['executeStatements']>;
|
|
11
7
|
};
|
|
12
8
|
|
|
13
|
-
export const createSqliteDatabaseViews = (
|
|
14
|
-
adapters: SqliteAdapter[],
|
|
15
|
-
): SqliteDatabaseView[] =>
|
|
9
|
+
export const createSqliteDatabaseViews = (adapters: SqliteAdapter[]): SqliteDatabaseView[] =>
|
|
16
10
|
adapters.flatMap((adapter) =>
|
|
17
11
|
adapter.databases.map((database) => ({
|
|
18
12
|
id: database.id,
|
|
@@ -18,15 +18,10 @@ export type RozeniteSqlitePluginOptions = {
|
|
|
18
18
|
|
|
19
19
|
const safeError = (error: unknown) => formatSqliteError(error);
|
|
20
20
|
|
|
21
|
-
const isExecuteStatementsError = (
|
|
22
|
-
error
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
('completedResults' in error || 'failedStatementIndex' in error);
|
|
26
|
-
|
|
27
|
-
export const useRozeniteSqlitePlugin = ({
|
|
28
|
-
adapters,
|
|
29
|
-
}: RozeniteSqlitePluginOptions) => {
|
|
21
|
+
const isExecuteStatementsError = (error: unknown): error is SqliteExecuteStatementsError =>
|
|
22
|
+
error instanceof Error && ('completedResults' in error || 'failedStatementIndex' in error);
|
|
23
|
+
|
|
24
|
+
export const useRozeniteSqlitePlugin = ({ adapters }: RozeniteSqlitePluginOptions) => {
|
|
30
25
|
const views = useMemo(() => createSqliteDatabaseViews(adapters), [adapters]);
|
|
31
26
|
|
|
32
27
|
useSqliteAgentTools(views);
|
|
@@ -45,8 +40,7 @@ export const useRozeniteSqlitePlugin = ({
|
|
|
45
40
|
databaseId: string,
|
|
46
41
|
task: () => Promise<T>,
|
|
47
42
|
): Promise<T> => {
|
|
48
|
-
const queue =
|
|
49
|
-
databaseQueuesRef.current.get(databaseId) ?? Promise.resolve();
|
|
43
|
+
const queue = databaseQueuesRef.current.get(databaseId) ?? Promise.resolve();
|
|
50
44
|
const next = queue.catch(() => undefined).then(task);
|
|
51
45
|
|
|
52
46
|
databaseQueuesRef.current.set(
|
|
@@ -70,10 +64,7 @@ export const useRozeniteSqlitePlugin = ({
|
|
|
70
64
|
return database;
|
|
71
65
|
};
|
|
72
66
|
|
|
73
|
-
const executeStatements = async (
|
|
74
|
-
databaseId: string,
|
|
75
|
-
statements: SqliteStatementInput[],
|
|
76
|
-
) => {
|
|
67
|
+
const executeStatements = async (databaseId: string, statements: SqliteStatementInput[]) => {
|
|
77
68
|
const database = resolveDatabase(databaseId);
|
|
78
69
|
const normalizedStatements = statements.map(({ sql, params }) => ({
|
|
79
70
|
sql: normalizeSingleStatementSql(sql),
|
|
@@ -130,128 +121,117 @@ export const useRozeniteSqlitePlugin = ({
|
|
|
130
121
|
);
|
|
131
122
|
|
|
132
123
|
subscriptionsRef.current.push(
|
|
133
|
-
client.onMessage(
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
},
|
|
154
|
-
),
|
|
124
|
+
client.onMessage('sqlite:query', async ({ requestId, databaseId, sql, params }) => {
|
|
125
|
+
try {
|
|
126
|
+
const result = await enqueueDatabaseTask(databaseId, () =>
|
|
127
|
+
executeSingleQuery(databaseId, sql, params),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
client.send('sqlite:query:result', {
|
|
131
|
+
requestId,
|
|
132
|
+
databaseId,
|
|
133
|
+
result,
|
|
134
|
+
});
|
|
135
|
+
} catch (error) {
|
|
136
|
+
client.send('sqlite:query:result', {
|
|
137
|
+
requestId,
|
|
138
|
+
databaseId,
|
|
139
|
+
error: safeError(error),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}),
|
|
155
143
|
);
|
|
156
144
|
|
|
157
145
|
subscriptionsRef.current.push(
|
|
158
|
-
client.onMessage(
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
146
|
+
client.onMessage('sqlite:execute-script', async ({ requestId, databaseId, sql }) => {
|
|
147
|
+
try {
|
|
148
|
+
const result = await enqueueDatabaseTask(databaseId, async () => {
|
|
149
|
+
const statementSegments = splitSqlStatements(sql);
|
|
150
|
+
|
|
151
|
+
if (statementSegments.length === 0) {
|
|
152
|
+
throw new Error('Query cannot be empty.');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const statementInputs = statementSegments.map((statement) => ({
|
|
156
|
+
sql: statement.text,
|
|
157
|
+
}));
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const execution = await executeStatements(databaseId, statementInputs);
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
statements: statementSegments.map((statement, index) => ({
|
|
164
|
+
index,
|
|
165
|
+
start: statement.start,
|
|
166
|
+
end: statement.end,
|
|
167
|
+
input: execution.inputs[index],
|
|
168
|
+
execution: {
|
|
169
|
+
input: execution.inputs[index],
|
|
170
|
+
result: execution.results[index],
|
|
171
|
+
},
|
|
172
|
+
})),
|
|
173
|
+
totalStatementCount: statementSegments.length,
|
|
174
|
+
failedStatementIndex: null,
|
|
175
|
+
};
|
|
176
|
+
} catch (error) {
|
|
177
|
+
if (!isExecuteStatementsError(error)) {
|
|
178
|
+
throw error;
|
|
167
179
|
}
|
|
168
180
|
|
|
169
|
-
const
|
|
170
|
-
|
|
181
|
+
const failedStatementIndex = Math.max(
|
|
182
|
+
0,
|
|
183
|
+
Math.min(
|
|
184
|
+
typeof error.failedStatementIndex === 'number'
|
|
185
|
+
? error.failedStatementIndex
|
|
186
|
+
: (error.completedResults?.length ?? 0),
|
|
187
|
+
statementSegments.length - 1,
|
|
188
|
+
),
|
|
189
|
+
);
|
|
190
|
+
const completedResults = (error.completedResults ?? []).slice(
|
|
191
|
+
0,
|
|
192
|
+
failedStatementIndex,
|
|
193
|
+
);
|
|
194
|
+
const completedStatements = completedResults.map((queryResult, index) => ({
|
|
195
|
+
index,
|
|
196
|
+
start: statementSegments[index].start,
|
|
197
|
+
end: statementSegments[index].end,
|
|
198
|
+
input: statementInputs[index],
|
|
199
|
+
execution: {
|
|
200
|
+
input: statementInputs[index],
|
|
201
|
+
result: queryResult,
|
|
202
|
+
},
|
|
171
203
|
}));
|
|
172
204
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
: (error.completedResults?.length ?? 0),
|
|
204
|
-
statementSegments.length - 1,
|
|
205
|
-
),
|
|
206
|
-
);
|
|
207
|
-
const completedResults = (error.completedResults ?? []).slice(
|
|
208
|
-
0,
|
|
209
|
-
failedStatementIndex,
|
|
210
|
-
);
|
|
211
|
-
const completedStatements = completedResults.map(
|
|
212
|
-
(queryResult, index) => ({
|
|
213
|
-
index,
|
|
214
|
-
start: statementSegments[index].start,
|
|
215
|
-
end: statementSegments[index].end,
|
|
216
|
-
input: statementInputs[index],
|
|
217
|
-
execution: {
|
|
218
|
-
input: statementInputs[index],
|
|
219
|
-
result: queryResult,
|
|
220
|
-
},
|
|
221
|
-
}),
|
|
222
|
-
);
|
|
223
|
-
|
|
224
|
-
return {
|
|
225
|
-
statements: [
|
|
226
|
-
...completedStatements,
|
|
227
|
-
{
|
|
228
|
-
index: failedStatementIndex,
|
|
229
|
-
start: statementSegments[failedStatementIndex].start,
|
|
230
|
-
end: statementSegments[failedStatementIndex].end,
|
|
231
|
-
input: statementInputs[failedStatementIndex],
|
|
232
|
-
error: safeError(error),
|
|
233
|
-
},
|
|
234
|
-
],
|
|
235
|
-
totalStatementCount: statementSegments.length,
|
|
236
|
-
failedStatementIndex,
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
client.send('sqlite:execute-script:result', {
|
|
242
|
-
requestId,
|
|
243
|
-
databaseId,
|
|
244
|
-
result,
|
|
245
|
-
});
|
|
246
|
-
} catch (error) {
|
|
247
|
-
client.send('sqlite:execute-script:result', {
|
|
248
|
-
requestId,
|
|
249
|
-
databaseId,
|
|
250
|
-
error: safeError(error),
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
},
|
|
254
|
-
),
|
|
205
|
+
return {
|
|
206
|
+
statements: [
|
|
207
|
+
...completedStatements,
|
|
208
|
+
{
|
|
209
|
+
index: failedStatementIndex,
|
|
210
|
+
start: statementSegments[failedStatementIndex].start,
|
|
211
|
+
end: statementSegments[failedStatementIndex].end,
|
|
212
|
+
input: statementInputs[failedStatementIndex],
|
|
213
|
+
error: safeError(error),
|
|
214
|
+
},
|
|
215
|
+
],
|
|
216
|
+
totalStatementCount: statementSegments.length,
|
|
217
|
+
failedStatementIndex,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
client.send('sqlite:execute-script:result', {
|
|
223
|
+
requestId,
|
|
224
|
+
databaseId,
|
|
225
|
+
result,
|
|
226
|
+
});
|
|
227
|
+
} catch (error) {
|
|
228
|
+
client.send('sqlite:execute-script:result', {
|
|
229
|
+
requestId,
|
|
230
|
+
databaseId,
|
|
231
|
+
error: safeError(error),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}),
|
|
255
235
|
);
|
|
256
236
|
|
|
257
237
|
return () => {
|
|
@@ -31,19 +31,15 @@ const executeSqlTool: AgentTool = {
|
|
|
31
31
|
},
|
|
32
32
|
sql: {
|
|
33
33
|
type: 'string',
|
|
34
|
-
description:
|
|
35
|
-
'SQL to execute. May contain multiple semicolon-separated statements.',
|
|
34
|
+
description: 'SQL to execute. May contain multiple semicolon-separated statements.',
|
|
36
35
|
},
|
|
37
36
|
},
|
|
38
37
|
required: ['databaseId', 'sql'],
|
|
39
38
|
},
|
|
40
39
|
};
|
|
41
40
|
|
|
42
|
-
const isExecuteStatementsError = (
|
|
43
|
-
error
|
|
44
|
-
): error is SqliteExecuteStatementsError =>
|
|
45
|
-
error instanceof Error &&
|
|
46
|
-
('completedResults' in error || 'failedStatementIndex' in error);
|
|
41
|
+
const isExecuteStatementsError = (error: unknown): error is SqliteExecuteStatementsError =>
|
|
42
|
+
error instanceof Error && ('completedResults' in error || 'failedStatementIndex' in error);
|
|
47
43
|
|
|
48
44
|
export const useSqliteAgentTools = (views: SqliteDatabaseView[]) => {
|
|
49
45
|
const resolveDatabase = useCallback(
|
|
@@ -52,9 +48,7 @@ export const useSqliteAgentTools = (views: SqliteDatabaseView[]) => {
|
|
|
52
48
|
|
|
53
49
|
if (!database) {
|
|
54
50
|
const available = views.map((v) => v.id).join(', ');
|
|
55
|
-
throw new Error(
|
|
56
|
-
`Unknown databaseId "${databaseId}". Available: ${available || '(none)'}`,
|
|
57
|
-
);
|
|
51
|
+
throw new Error(`Unknown databaseId "${databaseId}". Available: ${available || '(none)'}`);
|
|
58
52
|
}
|
|
59
53
|
|
|
60
54
|
return database;
|
|
@@ -122,10 +116,7 @@ export const useSqliteAgentTools = (views: SqliteDatabaseView[]) => {
|
|
|
122
116
|
statementSegments.length - 1,
|
|
123
117
|
),
|
|
124
118
|
);
|
|
125
|
-
const completedResults = (error.completedResults ?? []).slice(
|
|
126
|
-
0,
|
|
127
|
-
failedStatementIndex,
|
|
128
|
-
);
|
|
119
|
+
const completedResults = (error.completedResults ?? []).slice(0, failedStatementIndex);
|
|
129
120
|
|
|
130
121
|
return {
|
|
131
122
|
databaseId,
|
|
@@ -11,21 +11,16 @@ describe('sqlite bridge values', () => {
|
|
|
11
11
|
params: [new Uint8Array([1, 2, 255]), { nested: new Uint8Array([9, 8]) }],
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
-
expect(decodeSqliteBridgeValue(encodeSqliteBridgeValue(original))).toEqual(
|
|
15
|
-
original,
|
|
16
|
-
);
|
|
14
|
+
expect(decodeSqliteBridgeValue(encodeSqliteBridgeValue(original))).toEqual(original);
|
|
17
15
|
});
|
|
18
16
|
|
|
19
17
|
it('formats nested error details with code and cause information', () => {
|
|
20
|
-
const error = Object.assign(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
code: 'ERR_INTERNAL_SQLITE_ERROR',
|
|
25
|
-
reason: 'Invalid bind parameter',
|
|
26
|
-
},
|
|
18
|
+
const error = Object.assign(new Error("Calling the 'runAsync' function has failed"), {
|
|
19
|
+
cause: {
|
|
20
|
+
code: 'ERR_INTERNAL_SQLITE_ERROR',
|
|
21
|
+
reason: 'Invalid bind parameter',
|
|
27
22
|
},
|
|
28
|
-
);
|
|
23
|
+
});
|
|
29
24
|
|
|
30
25
|
expect(formatSqliteError(error)).toBe(
|
|
31
26
|
"Calling the 'runAsync' function has failed\nCaused by: [ERR_INTERNAL_SQLITE_ERROR] Invalid bind parameter",
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
getStatementAtCursor,
|
|
4
|
-
normalizeSingleStatementSql,
|
|
5
|
-
splitSqlStatements,
|
|
6
|
-
} from '../sql';
|
|
2
|
+
import { getStatementAtCursor, normalizeSingleStatementSql, splitSqlStatements } from '../sql';
|
|
7
3
|
|
|
8
4
|
describe('SQL statement helpers', () => {
|
|
9
5
|
it('finds the active statement when earlier statements contain comments and quoted semicolons', () => {
|
|
@@ -26,9 +22,9 @@ describe('SQL statement helpers', () => {
|
|
|
26
22
|
});
|
|
27
23
|
|
|
28
24
|
it('rejects multiple statements', () => {
|
|
29
|
-
expect(() =>
|
|
30
|
-
|
|
31
|
-
)
|
|
25
|
+
expect(() => normalizeSingleStatementSql('SELECT * FROM projects; DELETE FROM logs;')).toThrow(
|
|
26
|
+
'Only a single SQL statement is supported in v1.',
|
|
27
|
+
);
|
|
32
28
|
});
|
|
33
29
|
|
|
34
30
|
it('splits statements while preserving source offsets', () => {
|
|
@@ -8,9 +8,7 @@ type SqliteEncodedBinaryValue = {
|
|
|
8
8
|
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
9
9
|
!!value && typeof value === 'object';
|
|
10
10
|
|
|
11
|
-
const isEncodedBinaryValue = (
|
|
12
|
-
value: unknown,
|
|
13
|
-
): value is SqliteEncodedBinaryValue =>
|
|
11
|
+
const isEncodedBinaryValue = (value: unknown): value is SqliteEncodedBinaryValue =>
|
|
14
12
|
isRecord(value) &&
|
|
15
13
|
value[SQLITE_BRIDGE_BINARY_TYPE] === true &&
|
|
16
14
|
Array.isArray(value.data) &&
|
|
@@ -114,8 +112,7 @@ const describeError = (error: unknown): string | null => {
|
|
|
114
112
|
const reason = getStringField(error, 'reason');
|
|
115
113
|
const message = error.message.trim();
|
|
116
114
|
const detailParts = [message || null, reason].filter(
|
|
117
|
-
(part, index, parts): part is string =>
|
|
118
|
-
!!part && parts.indexOf(part) === index,
|
|
115
|
+
(part, index, parts): part is string => !!part && parts.indexOf(part) === index,
|
|
119
116
|
);
|
|
120
117
|
const detail = detailParts.join(' | ') || error.name;
|
|
121
118
|
|
package/src/shared/sql.ts
CHANGED
|
@@ -324,9 +324,7 @@ export const getStatementAtCursor = (sql: string, cursor: number) => {
|
|
|
324
324
|
return null;
|
|
325
325
|
}
|
|
326
326
|
|
|
327
|
-
const match = segments.find(
|
|
328
|
-
(segment) => cursor >= segment.start && cursor <= segment.end + 1,
|
|
329
|
-
);
|
|
327
|
+
const match = segments.find((segment) => cursor >= segment.start && cursor <= segment.end + 1);
|
|
330
328
|
|
|
331
329
|
return match ?? segments[0];
|
|
332
330
|
};
|
|
@@ -413,8 +411,6 @@ export const statementReturnsRows = (statementType: SqliteStatementType) =>
|
|
|
413
411
|
statementType === 'explain' ||
|
|
414
412
|
statementType === 'with';
|
|
415
413
|
|
|
416
|
-
export const quoteSqlIdentifier = (identifier: string) =>
|
|
417
|
-
`"${identifier.replace(/"/g, '""')}"`;
|
|
414
|
+
export const quoteSqlIdentifier = (identifier: string) => `"${identifier.replace(/"/g, '""')}"`;
|
|
418
415
|
|
|
419
|
-
export const escapeSqlString = (value: string) =>
|
|
420
|
-
`'${value.replace(/'/g, "''")}'`;
|
|
416
|
+
export const escapeSqlString = (value: string) => `'${value.replace(/'/g, "''")}'`;
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import type {
|
|
3
|
-
SqliteColumnInfo,
|
|
4
|
-
SqliteEntity,
|
|
5
|
-
SqliteSchema,
|
|
6
|
-
} from '../sqlite-introspection';
|
|
2
|
+
import type { SqliteColumnInfo, SqliteEntity, SqliteSchema } from '../sqlite-introspection';
|
|
7
3
|
import {
|
|
8
4
|
buildSqlCompletionSchema,
|
|
9
5
|
createSqlEditorColumnCache,
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
buildDropAllEntitiesSql,
|
|
4
|
+
buildDropEntitySql,
|
|
5
|
+
buildSetForeignKeysSql,
|
|
6
|
+
isForeignKeysEnabled,
|
|
7
|
+
orderEntitiesForDrop,
|
|
8
|
+
SQLITE_READ_FOREIGN_KEYS_SQL,
|
|
9
|
+
type SqliteDropTarget,
|
|
10
|
+
} from '../sqlite-drop-mutations';
|
|
11
|
+
|
|
12
|
+
describe('buildDropEntitySql', () => {
|
|
13
|
+
it('builds a DROP TABLE statement for a table entity', () => {
|
|
14
|
+
expect(
|
|
15
|
+
buildDropEntitySql({
|
|
16
|
+
schemaName: 'main',
|
|
17
|
+
name: 'projects',
|
|
18
|
+
type: 'table',
|
|
19
|
+
}),
|
|
20
|
+
).toBe('DROP TABLE "main"."projects"');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('builds a DROP VIEW statement for a view entity', () => {
|
|
24
|
+
expect(
|
|
25
|
+
buildDropEntitySql({
|
|
26
|
+
schemaName: 'main',
|
|
27
|
+
name: 'active_projects',
|
|
28
|
+
type: 'view',
|
|
29
|
+
}),
|
|
30
|
+
).toBe('DROP VIEW "main"."active_projects"');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('quotes identifiers containing double quotes by doubling them', () => {
|
|
34
|
+
expect(
|
|
35
|
+
buildDropEntitySql({
|
|
36
|
+
schemaName: 'main',
|
|
37
|
+
name: 'weird"table',
|
|
38
|
+
type: 'table',
|
|
39
|
+
}),
|
|
40
|
+
).toBe('DROP TABLE "main"."weird""table"');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('quotes identifiers containing dots so they are not read as separators', () => {
|
|
44
|
+
expect(
|
|
45
|
+
buildDropEntitySql({
|
|
46
|
+
schemaName: 'main',
|
|
47
|
+
name: 'schema.like.name',
|
|
48
|
+
type: 'table',
|
|
49
|
+
}),
|
|
50
|
+
).toBe('DROP TABLE "main"."schema.like.name"');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('quotes schema names containing double quotes and dots as well', () => {
|
|
54
|
+
expect(
|
|
55
|
+
buildDropEntitySql({
|
|
56
|
+
schemaName: 'weird"schema.name',
|
|
57
|
+
name: 'projects',
|
|
58
|
+
type: 'view',
|
|
59
|
+
}),
|
|
60
|
+
).toBe('DROP VIEW "weird""schema.name"."projects"');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('orderEntitiesForDrop', () => {
|
|
65
|
+
it('moves all views before all tables while preserving relative order', () => {
|
|
66
|
+
const entities: SqliteDropTarget[] = [
|
|
67
|
+
{ schemaName: 'main', name: 'a_table', type: 'table' },
|
|
68
|
+
{ schemaName: 'main', name: 'b_view', type: 'view' },
|
|
69
|
+
{ schemaName: 'main', name: 'c_table', type: 'table' },
|
|
70
|
+
{ schemaName: 'main', name: 'd_view', type: 'view' },
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
expect(orderEntitiesForDrop(entities).map((entity) => entity.name)).toEqual([
|
|
74
|
+
'b_view',
|
|
75
|
+
'd_view',
|
|
76
|
+
'a_table',
|
|
77
|
+
'c_table',
|
|
78
|
+
]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('returns an empty array for an empty database', () => {
|
|
82
|
+
expect(orderEntitiesForDrop([])).toEqual([]);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('buildDropAllEntitiesSql', () => {
|
|
87
|
+
it('emits DROP VIEW statements before DROP TABLE statements', () => {
|
|
88
|
+
const entities: SqliteDropTarget[] = [
|
|
89
|
+
{ schemaName: 'main', name: 'projects', type: 'table' },
|
|
90
|
+
{ schemaName: 'main', name: 'active_projects', type: 'view' },
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
expect(buildDropAllEntitiesSql(entities)).toBe(
|
|
94
|
+
'DROP VIEW "main"."active_projects";\nDROP TABLE "main"."projects";',
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('quotes every identifier in the generated script', () => {
|
|
99
|
+
const entities: SqliteDropTarget[] = [
|
|
100
|
+
{ schemaName: 'main', name: 'weird"table', type: 'table' },
|
|
101
|
+
{ schemaName: 'main', name: 'schema.view', type: 'view' },
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
expect(buildDropAllEntitiesSql(entities)).toBe(
|
|
105
|
+
'DROP VIEW "main"."schema.view";\nDROP TABLE "main"."weird""table";',
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('returns an empty string for an empty database', () => {
|
|
110
|
+
expect(buildDropAllEntitiesSql([])).toBe('');
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('foreign key pragma helpers', () => {
|
|
115
|
+
it('exposes the read-only pragma statement', () => {
|
|
116
|
+
expect(SQLITE_READ_FOREIGN_KEYS_SQL).toBe('PRAGMA foreign_keys');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('builds a statement enabling foreign keys', () => {
|
|
120
|
+
expect(buildSetForeignKeysSql(true)).toBe('PRAGMA foreign_keys = ON');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('builds a statement disabling foreign keys', () => {
|
|
124
|
+
expect(buildSetForeignKeysSql(false)).toBe('PRAGMA foreign_keys = OFF');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('treats a foreign_keys value of 1 as enabled', () => {
|
|
128
|
+
expect(isForeignKeysEnabled([{ foreign_keys: 1 }])).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('treats a foreign_keys value of 0 as disabled', () => {
|
|
132
|
+
expect(isForeignKeysEnabled([{ foreign_keys: 0 }])).toBe(false);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('treats an empty result set as disabled', () => {
|
|
136
|
+
expect(isForeignKeysEnabled([])).toBe(false);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -166,9 +166,7 @@ describe('sqlite row mutation helpers', () => {
|
|
|
166
166
|
it('disables row mutations for WITHOUT ROWID tables that lack a primary key', () => {
|
|
167
167
|
expect(
|
|
168
168
|
getRowMutationDescriptor(
|
|
169
|
-
buildEntity(
|
|
170
|
-
'CREATE TABLE projects(title TEXT, slug TEXT) WITHOUT ROWID',
|
|
171
|
-
),
|
|
169
|
+
buildEntity('CREATE TABLE projects(title TEXT, slug TEXT) WITHOUT ROWID'),
|
|
172
170
|
[
|
|
173
171
|
{
|
|
174
172
|
cid: 0,
|
|
@@ -23,13 +23,7 @@ describe('sqlite table column order helpers', () => {
|
|
|
23
23
|
normalizeTableColumnOrder({
|
|
24
24
|
columnIds: [SQLITE_ROW_NUMBER_COLUMN_ID, 'name', 'type'],
|
|
25
25
|
fixedLeadingColumnIds: [SQLITE_ROW_NUMBER_COLUMN_ID],
|
|
26
|
-
storedColumnOrder: [
|
|
27
|
-
'type',
|
|
28
|
-
SQLITE_ROW_NUMBER_COLUMN_ID,
|
|
29
|
-
'stale',
|
|
30
|
-
'name',
|
|
31
|
-
'type',
|
|
32
|
-
],
|
|
26
|
+
storedColumnOrder: ['type', SQLITE_ROW_NUMBER_COLUMN_ID, 'stale', 'name', 'type'],
|
|
33
27
|
}),
|
|
34
28
|
).toEqual([SQLITE_ROW_NUMBER_COLUMN_ID, 'type', 'name']);
|
|
35
29
|
});
|
|
@@ -49,12 +43,7 @@ describe('sqlite table column order helpers', () => {
|
|
|
49
43
|
reorderTableColumnOrder({
|
|
50
44
|
columnIds: [SQLITE_ROW_NUMBER_COLUMN_ID, 'name', 'type', 'extra'],
|
|
51
45
|
fixedLeadingColumnIds: [SQLITE_ROW_NUMBER_COLUMN_ID],
|
|
52
|
-
storedColumnOrder: [
|
|
53
|
-
SQLITE_ROW_NUMBER_COLUMN_ID,
|
|
54
|
-
'name',
|
|
55
|
-
'type',
|
|
56
|
-
'extra',
|
|
57
|
-
],
|
|
46
|
+
storedColumnOrder: [SQLITE_ROW_NUMBER_COLUMN_ID, 'name', 'type', 'extra'],
|
|
58
47
|
activeColumnId: 'extra',
|
|
59
48
|
overColumnId: 'name',
|
|
60
49
|
}),
|
|
@@ -73,11 +62,7 @@ describe('sqlite table column order helpers', () => {
|
|
|
73
62
|
});
|
|
74
63
|
|
|
75
64
|
it('builds stable table ids for entity and query surfaces', () => {
|
|
76
|
-
expect(buildEntityTableId('data', 'db-1', 'main', 'users')).toBe(
|
|
77
|
-
|
|
78
|
-
);
|
|
79
|
-
expect(buildQueryTableId('db-1', ['id', 'name'])).toBe(
|
|
80
|
-
'query:db-1:["id","name"]',
|
|
81
|
-
);
|
|
65
|
+
expect(buildEntityTableId('data', 'db-1', 'main', 'users')).toBe('data:db-1:main:users');
|
|
66
|
+
expect(buildQueryTableId('db-1', ['id', 'name'])).toBe('query:db-1:["id","name"]');
|
|
82
67
|
});
|
|
83
68
|
});
|
|
@@ -10,12 +10,7 @@ type CellDetailDrawerProps = {
|
|
|
10
10
|
onClose: () => void;
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
-
export const CellDetailDrawer = ({
|
|
14
|
-
value,
|
|
15
|
-
title,
|
|
16
|
-
isOpen,
|
|
17
|
-
onClose,
|
|
18
|
-
}: CellDetailDrawerProps) => {
|
|
13
|
+
export const CellDetailDrawer = ({ value, title, isOpen, onClose }: CellDetailDrawerProps) => {
|
|
19
14
|
const overlay = useOverlayState({
|
|
20
15
|
isOpen,
|
|
21
16
|
onOpenChange: (open: boolean) => {
|