@rozenite/sqlite-plugin 1.12.0 → 2.0.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.
@@ -22,8 +22,8 @@
22
22
  <script>
23
23
  var __ROZENITE_PANEL__ = true;
24
24
  </script>
25
- <script type="module" crossorigin src="../devtools/assets/panel-DjRs5NTl.js"></script>
26
- <link rel="stylesheet" crossorigin href="../devtools/assets/panel-CIU0JBOs.css">
25
+ <script type="module" crossorigin src="../devtools/assets/panel-CsA6Ejak.js"></script>
26
+ <link rel="stylesheet" crossorigin href="../devtools/assets/panel-BarPLQ4-.css">
27
27
  </head>
28
28
  <body>
29
29
  <div id="root"></div>
@@ -1,4 +1,4 @@
1
- import { RozeniteDevToolsClient } from '@rozenite/plugin-bridge';
1
+ import { RozeniteDevToolsRequestClient } from '@rozenite/plugin-bridge';
2
2
 
3
3
  export declare let classifySqlStatement: typeof classifySqlStatement_2;
4
4
 
@@ -203,6 +203,6 @@ declare const statementReturnsRows_2: (statementType: SqliteStatementType) => st
203
203
 
204
204
  export declare let useRozeniteSqlitePlugin: typeof useRozeniteSqlitePlugin_2;
205
205
 
206
- declare const useRozeniteSqlitePlugin_2: ({ adapters, }: RozeniteSqlitePluginOptions) => RozeniteDevToolsClient<SqliteEventMap> | null;
206
+ declare const useRozeniteSqlitePlugin_2: ({ adapters, }: RozeniteSqlitePluginOptions) => RozeniteDevToolsRequestClient<SqliteEventMap> | null;
207
207
 
208
208
  export { }
@@ -1 +1 @@
1
- {"name":"@rozenite/sqlite-plugin","version":"1.12.0","description":"SQLite inspector for Rozenite.","panels":[{"name":"SQLite","source":"/devtools/panel.html"}]}
1
+ {"name":"@rozenite/sqlite-plugin","version":"2.0.0","description":"SQLite inspector for Rozenite.","panels":[{"name":"SQLite","source":"/devtools/panel.html"}]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rozenite/sqlite-plugin",
3
- "version": "1.12.0",
3
+ "version": "2.0.0",
4
4
  "description": "SQLite inspector for Rozenite.",
5
5
  "type": "module",
6
6
  "main": "./dist/react-native/index.cjs",
@@ -15,8 +15,8 @@
15
15
  "url": "https://github.com/callstackincubator/rozenite.git"
16
16
  },
17
17
  "dependencies": {
18
- "@rozenite/agent-bridge": "1.12.0",
19
- "@rozenite/plugin-bridge": "1.12.0"
18
+ "@rozenite/agent-bridge": "2.0.0",
19
+ "@rozenite/plugin-bridge": "2.0.0"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@dnd-kit/core": "^6.3.1",
@@ -31,26 +31,22 @@
31
31
  "@heroui/react": "^3.0.1",
32
32
  "@heroui/styles": "^3.0.1",
33
33
  "@lezer/highlight": "^1.2.3",
34
- "@tailwindcss/postcss": "^4.2.2",
35
34
  "@tanstack/react-table": "^8.21.3",
36
35
  "@tanstack/react-virtual": "^3.0.0",
37
- "@types/react": "~19.2.2",
38
- "autoprefixer": "^10.4.21",
36
+ "@types/react": "~19.2.3",
39
37
  "expo-sqlite": "^55.0.11",
40
38
  "lucide-react": "^0.263.1",
41
- "postcss": "^8.5.6",
42
- "react": "19.2.0",
43
- "react-dom": "19.2.0",
39
+ "react": "19.2.3",
40
+ "react-dom": "19.2.3",
44
41
  "react-json-tree": "^0.20.0",
45
- "react-native": "0.83.1",
42
+ "react-native": "0.86.0",
46
43
  "react-native-web": "^0.21.2",
47
44
  "sql-formatter": "^15.7.2",
48
- "tailwindcss": "^4.2.2",
49
45
  "typescript": "~5.9.3",
50
46
  "vite": "^7.3.1",
51
47
  "vitest": "^3.2.4",
52
- "@rozenite/vite-plugin": "1.12.0",
53
- "rozenite": "1.12.0"
48
+ "@rozenite/vite-plugin": "2.0.0",
49
+ "rozenite": "2.0.0"
54
50
  },
55
51
  "peerDependencies": {
56
52
  "expo-sqlite": "*",
@@ -1,5 +1,8 @@
1
1
  import { useCallback } from 'react';
2
- import { useRozenitePluginAgentTool, type AgentTool } from '@rozenite/agent-bridge';
2
+ import {
3
+ useRozenitePluginAgentTool,
4
+ type AgentTool,
5
+ } from '@rozenite/agent-bridge';
3
6
  import { formatSqliteError } from '../shared/bridge-values';
4
7
  import { normalizeSingleStatementSql, splitSqlStatements } from '../shared/sql';
5
8
  import type { SqliteExecuteStatementsError } from '../shared/types';
@@ -0,0 +1,135 @@
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', 'd_view', 'a_table', 'c_table'],
75
+ );
76
+ });
77
+
78
+ it('returns an empty array for an empty database', () => {
79
+ expect(orderEntitiesForDrop([])).toEqual([]);
80
+ });
81
+ });
82
+
83
+ describe('buildDropAllEntitiesSql', () => {
84
+ it('emits DROP VIEW statements before DROP TABLE statements', () => {
85
+ const entities: SqliteDropTarget[] = [
86
+ { schemaName: 'main', name: 'projects', type: 'table' },
87
+ { schemaName: 'main', name: 'active_projects', type: 'view' },
88
+ ];
89
+
90
+ expect(buildDropAllEntitiesSql(entities)).toBe(
91
+ 'DROP VIEW "main"."active_projects";\nDROP TABLE "main"."projects";',
92
+ );
93
+ });
94
+
95
+ it('quotes every identifier in the generated script', () => {
96
+ const entities: SqliteDropTarget[] = [
97
+ { schemaName: 'main', name: 'weird"table', type: 'table' },
98
+ { schemaName: 'main', name: 'schema.view', type: 'view' },
99
+ ];
100
+
101
+ expect(buildDropAllEntitiesSql(entities)).toBe(
102
+ 'DROP VIEW "main"."schema.view";\nDROP TABLE "main"."weird""table";',
103
+ );
104
+ });
105
+
106
+ it('returns an empty string for an empty database', () => {
107
+ expect(buildDropAllEntitiesSql([])).toBe('');
108
+ });
109
+ });
110
+
111
+ describe('foreign key pragma helpers', () => {
112
+ it('exposes the read-only pragma statement', () => {
113
+ expect(SQLITE_READ_FOREIGN_KEYS_SQL).toBe('PRAGMA foreign_keys');
114
+ });
115
+
116
+ it('builds a statement enabling foreign keys', () => {
117
+ expect(buildSetForeignKeysSql(true)).toBe('PRAGMA foreign_keys = ON');
118
+ });
119
+
120
+ it('builds a statement disabling foreign keys', () => {
121
+ expect(buildSetForeignKeysSql(false)).toBe('PRAGMA foreign_keys = OFF');
122
+ });
123
+
124
+ it('treats a foreign_keys value of 1 as enabled', () => {
125
+ expect(isForeignKeysEnabled([{ foreign_keys: 1 }])).toBe(true);
126
+ });
127
+
128
+ it('treats a foreign_keys value of 0 as disabled', () => {
129
+ expect(isForeignKeysEnabled([{ foreign_keys: 0 }])).toBe(false);
130
+ });
131
+
132
+ it('treats an empty result set as disabled', () => {
133
+ expect(isForeignKeysEnabled([])).toBe(false);
134
+ });
135
+ });
@@ -399,6 +399,18 @@
399
399
  flex-shrink: 0;
400
400
  }
401
401
 
402
+ .sqlite-button-danger {
403
+ border-color: rgba(255, 100, 100, 0.35);
404
+ background: rgba(255, 90, 90, 0.14);
405
+ color: rgba(255, 214, 214, 0.95);
406
+ }
407
+
408
+ .sqlite-button-danger:hover:not(:disabled) {
409
+ border-color: rgba(255, 120, 120, 0.55);
410
+ background: rgba(255, 90, 90, 0.22);
411
+ color: white;
412
+ }
413
+
402
414
  .sqlite-badge,
403
415
  .sqlite-chip,
404
416
  .sqlite-count-pill {
package/src/ui/panel.tsx CHANGED
@@ -65,6 +65,17 @@ import {
65
65
  } from './sqlite-introspection';
66
66
  import { QueryResultTable } from './query-result-table';
67
67
  import { SqliteRowDeleteModal } from './sqlite-row-delete-modal';
68
+ import {
69
+ SqliteDropModal,
70
+ type SqliteDropModalTarget,
71
+ } from './sqlite-drop-modal';
72
+ import {
73
+ buildDropAllEntitiesSql,
74
+ buildDropEntitySql,
75
+ buildSetForeignKeysSql,
76
+ isForeignKeysEnabled,
77
+ SQLITE_READ_FOREIGN_KEYS_SQL,
78
+ } from './sqlite-drop-mutations';
68
79
  import { SqliteDataTable } from './sqlite-data-table';
69
80
  import { SqliteRowEditModal } from './sqlite-row-edit-modal';
70
81
  import { SqlEditor, type SqlEditorHandle } from './sql-editor';
@@ -148,6 +159,15 @@ type ActiveRowMutationState = {
148
159
  rowIndex: number;
149
160
  } | null;
150
161
 
162
+ /**
163
+ * The drop dialog operates on the target captured when it opened, so that a
164
+ * mid-dialog explorer reload cannot repoint it at a different object.
165
+ */
166
+ type ActiveDropState = {
167
+ databaseId: string;
168
+ target: SqliteDropModalTarget;
169
+ } | null;
170
+
151
171
  const DEFAULT_QUERY =
152
172
  'SELECT name, type FROM sqlite_schema ORDER BY type, name';
153
173
  const DEFAULT_QUERY_LIMIT = 100;
@@ -164,6 +184,14 @@ const DEFAULT_EXPLORER_STATE: ExplorerState = {
164
184
  loaded: false,
165
185
  };
166
186
 
187
+ const DEFAULT_DROP_TARGET: SqliteDropModalTarget = {
188
+ kind: 'entity',
189
+ entityType: 'table',
190
+ qualifiedName: '',
191
+ confirmationValue: '',
192
+ sql: '',
193
+ };
194
+
167
195
  const joinClassNames = (
168
196
  ...classNames: Array<string | false | null | undefined>
169
197
  ) => classNames.filter(Boolean).join(' ');
@@ -315,6 +343,7 @@ const iconButtonClassName =
315
343
  'sqlite-icon-button inline-flex h-10 w-10 items-center justify-center rounded-xl';
316
344
  const primaryIconButtonClassName = `${iconButtonClassName} sqlite-button-primary`;
317
345
  const secondaryIconButtonClassName = `${iconButtonClassName} sqlite-button-secondary`;
346
+ const dangerIconButtonClassName = `${iconButtonClassName} sqlite-button-danger`;
318
347
  const ghostIconButtonClassName = `${iconButtonClassName} sqlite-button-ghost`;
319
348
 
320
349
  const renderEmptyState = (
@@ -418,6 +447,7 @@ export default function SqlitePanel() {
418
447
  >({});
419
448
  const [editingRow, setEditingRow] = useState<ActiveRowMutationState>(null);
420
449
  const [deletingRow, setDeletingRow] = useState<ActiveRowMutationState>(null);
450
+ const [activeDrop, setActiveDrop] = useState<ActiveDropState>(null);
421
451
 
422
452
  const [objectSearch, setObjectSearch] = useState('');
423
453
  const [dataSearch, setDataSearch] = useState('');
@@ -1202,6 +1232,149 @@ export default function SqlitePanel() {
1202
1232
  selectedEntity,
1203
1233
  ]);
1204
1234
 
1235
+ const handleOpenDropEntity = useCallback(() => {
1236
+ if (!selectedDatabaseId || !selectedEntity) {
1237
+ return;
1238
+ }
1239
+
1240
+ setActiveDrop({
1241
+ databaseId: selectedDatabaseId,
1242
+ target: {
1243
+ kind: 'entity',
1244
+ entityType: selectedEntity.type,
1245
+ qualifiedName: `${selectedEntity.schemaName}.${selectedEntity.name}`,
1246
+ confirmationValue: selectedEntity.name,
1247
+ sql: `${buildDropEntitySql(selectedEntity)};`,
1248
+ },
1249
+ });
1250
+ }, [selectedDatabaseId, selectedEntity]);
1251
+
1252
+ const handleOpenDropAllEntities = useCallback(() => {
1253
+ if (!selectedDatabaseId || !selectedDatabase) {
1254
+ return;
1255
+ }
1256
+
1257
+ const tableCount = entities.filter(
1258
+ (entity) => entity.type === 'table',
1259
+ ).length;
1260
+ const viewCount = entities.filter(
1261
+ (entity) => entity.type === 'view',
1262
+ ).length;
1263
+
1264
+ setActiveDrop({
1265
+ databaseId: selectedDatabaseId,
1266
+ target: {
1267
+ kind: 'database',
1268
+ databaseName: selectedDatabase.name,
1269
+ confirmationValue: selectedDatabase.name,
1270
+ tableCount,
1271
+ viewCount,
1272
+ sql: buildDropAllEntitiesSql(entities),
1273
+ },
1274
+ });
1275
+ }, [entities, selectedDatabase, selectedDatabaseId]);
1276
+
1277
+ const dropSelectedEntity = useCallback(
1278
+ async (databaseId: string, sql: string) => {
1279
+ await requestQuery({ databaseId, sql });
1280
+ },
1281
+ [requestQuery],
1282
+ );
1283
+
1284
+ const dropAllEntities = useCallback(
1285
+ async (databaseId: string, sql: string) => {
1286
+ // Dropping a parent table before its children fails while foreign key
1287
+ // enforcement is on, so turn it off for the sweep. The pragma is
1288
+ // per-connection and that connection belongs to the running app, so the
1289
+ // original value has to be restored on every path out of here.
1290
+ const foreignKeysResult = await requestQuery({
1291
+ databaseId,
1292
+ sql: SQLITE_READ_FOREIGN_KEYS_SQL,
1293
+ });
1294
+ const foreignKeysWereEnabled = isForeignKeysEnabled(
1295
+ foreignKeysResult.rows,
1296
+ );
1297
+
1298
+ if (foreignKeysWereEnabled) {
1299
+ await requestQuery({
1300
+ databaseId,
1301
+ sql: buildSetForeignKeysSql(false),
1302
+ });
1303
+ }
1304
+
1305
+ let dropError: unknown = null;
1306
+
1307
+ try {
1308
+ const scriptResult = await requestScriptExecution({
1309
+ databaseId,
1310
+ sql,
1311
+ });
1312
+
1313
+ if (scriptResult.failedStatementIndex != null) {
1314
+ const failedStatement = scriptResult.statements.find(
1315
+ (statement) =>
1316
+ statement.index === scriptResult.failedStatementIndex,
1317
+ );
1318
+ throw new Error(failedStatement?.error ?? 'Script execution failed.');
1319
+ }
1320
+ } catch (error) {
1321
+ dropError = error;
1322
+ }
1323
+
1324
+ if (foreignKeysWereEnabled) {
1325
+ try {
1326
+ await requestQuery({
1327
+ databaseId,
1328
+ sql: buildSetForeignKeysSql(true),
1329
+ });
1330
+ } catch (restoreError) {
1331
+ // The drop error, if any, takes priority: it's the reason the user
1332
+ // is looking at this modal. Only surface the restore failure when
1333
+ // the drop itself succeeded, since that's a new, silent problem.
1334
+ if (!dropError) {
1335
+ throw restoreError instanceof Error
1336
+ ? restoreError
1337
+ : new Error(String(restoreError));
1338
+ }
1339
+ }
1340
+ }
1341
+
1342
+ if (dropError) {
1343
+ throw dropError instanceof Error
1344
+ ? dropError
1345
+ : new Error(String(dropError));
1346
+ }
1347
+ },
1348
+ [requestQuery, requestScriptExecution],
1349
+ );
1350
+
1351
+ const handleConfirmDrop = useCallback(async () => {
1352
+ if (!activeDrop) {
1353
+ throw new Error('The drop target is no longer available.');
1354
+ }
1355
+
1356
+ // Execute the target captured when the dialog opened rather than the
1357
+ // current selection: an app reload can fire `sqlite:ready` mid-dialog,
1358
+ // which reloads the explorer and may move the selection elsewhere. The
1359
+ // user confirmed by typing this object's name, so this is the object that
1360
+ // has to be dropped.
1361
+ const { databaseId, target } = activeDrop;
1362
+
1363
+ if (!target.sql) {
1364
+ throw new Error('There is nothing to drop.');
1365
+ }
1366
+
1367
+ if (target.kind === 'entity') {
1368
+ await dropSelectedEntity(databaseId, target.sql);
1369
+ } else {
1370
+ await dropAllEntities(databaseId, target.sql);
1371
+ }
1372
+
1373
+ setActiveDrop(null);
1374
+ setSelectedEntityKey(null);
1375
+ await refreshExplorerData();
1376
+ }, [activeDrop, dropAllEntities, dropSelectedEntity, refreshExplorerData]);
1377
+
1205
1378
  const getActiveStatement = useCallback(() => {
1206
1379
  const cursorPosition =
1207
1380
  editorRef.current?.getSelection().start ?? querySelection.start;
@@ -2287,6 +2460,17 @@ export default function SqlitePanel() {
2287
2460
  )}
2288
2461
  />
2289
2462
  </button>
2463
+ <button
2464
+ type="button"
2465
+ className={dangerIconButtonClassName}
2466
+ onClick={handleOpenDropEntity}
2467
+ aria-label={
2468
+ selectedEntity.type === 'view' ? 'Drop view' : 'Drop table'
2469
+ }
2470
+ title={selectedEntity.type === 'view' ? 'Drop view' : 'Drop table'}
2471
+ >
2472
+ <Trash2 aria-hidden="true" className="h-4 w-4" />
2473
+ </button>
2290
2474
  </div>
2291
2475
  </header>
2292
2476
 
@@ -2455,6 +2639,29 @@ export default function SqlitePanel() {
2455
2639
  )}
2456
2640
  />
2457
2641
  </button>
2642
+ <button
2643
+ type="button"
2644
+ className={dangerIconButtonClassName}
2645
+ aria-label={
2646
+ selectedDatabase
2647
+ ? `Drop all objects in ${selectedDatabase.name}`
2648
+ : 'Drop all objects'
2649
+ }
2650
+ title={
2651
+ selectedDatabase
2652
+ ? `Drop all objects in ${selectedDatabase.name}`
2653
+ : 'Drop all objects'
2654
+ }
2655
+ onClick={handleOpenDropAllEntities}
2656
+ disabled={
2657
+ !selectedDatabaseId ||
2658
+ entities.length === 0 ||
2659
+ databaseLoading ||
2660
+ entityLoading
2661
+ }
2662
+ >
2663
+ <Trash2 aria-hidden="true" className="h-4 w-4" />
2664
+ </button>
2458
2665
  </div>
2459
2666
  </header>
2460
2667
 
@@ -2809,6 +3016,13 @@ export default function SqlitePanel() {
2809
3016
  onClose={() => setDeletingRow(null)}
2810
3017
  onDelete={handleDeleteRow}
2811
3018
  />
3019
+
3020
+ <SqliteDropModal
3021
+ isOpen={!!activeDrop}
3022
+ target={activeDrop?.target ?? DEFAULT_DROP_TARGET}
3023
+ onClose={() => setActiveDrop(null)}
3024
+ onConfirm={handleConfirmDrop}
3025
+ />
2812
3026
  </div>
2813
3027
  </div>
2814
3028
  );