sqlite-hub 0.1.3
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/.npmingnore +4 -0
- package/README.md +46 -0
- package/assets/images/logo.webp +0 -0
- package/assets/images/logo_extrasmall.webp +0 -0
- package/assets/images/logo_raw.png +0 -0
- package/assets/images/logo_small.webp +0 -0
- package/assets/mockups/connections.png +0 -0
- package/assets/mockups/data.png +0 -0
- package/assets/mockups/data_edit.png +0 -0
- package/assets/mockups/home.png +0 -0
- package/assets/mockups/overview.png +0 -0
- package/assets/mockups/sql_editor.png +0 -0
- package/assets/mockups/structure.png +0 -0
- package/bin/sqlite-hub.js +116 -0
- package/changelog.md +3 -0
- package/data/.gitkeep +0 -0
- package/index.html +100 -0
- package/js/api.js +193 -0
- package/js/app.js +520 -0
- package/js/components/actionBar.js +8 -0
- package/js/components/appShell.js +17 -0
- package/js/components/badges.js +5 -0
- package/js/components/bottomTabs.js +37 -0
- package/js/components/connectionCard.js +106 -0
- package/js/components/dataGrid.js +47 -0
- package/js/components/emptyState.js +159 -0
- package/js/components/metricCard.js +32 -0
- package/js/components/modal.js +317 -0
- package/js/components/pageHeader.js +33 -0
- package/js/components/queryEditor.js +121 -0
- package/js/components/queryResults.js +107 -0
- package/js/components/rowEditorPanel.js +164 -0
- package/js/components/sidebar.js +57 -0
- package/js/components/statusBar.js +39 -0
- package/js/components/toast.js +39 -0
- package/js/components/topNav.js +27 -0
- package/js/router.js +66 -0
- package/js/store.js +1092 -0
- package/js/utils/format.js +179 -0
- package/js/views/connections.js +133 -0
- package/js/views/data.js +400 -0
- package/js/views/editor.js +259 -0
- package/js/views/landing.js +11 -0
- package/js/views/overview.js +220 -0
- package/js/views/settings.js +109 -0
- package/js/views/structure.js +242 -0
- package/package.json +18 -0
- package/publish_brew.sh +444 -0
- package/publish_npm.sh +241 -0
- package/server/routes/connections.js +146 -0
- package/server/routes/data.js +59 -0
- package/server/routes/export.js +25 -0
- package/server/routes/overview.js +39 -0
- package/server/routes/settings.js +50 -0
- package/server/routes/sql.js +50 -0
- package/server/routes/structure.js +38 -0
- package/server/server.js +136 -0
- package/server/services/sqlite/connectionManager.js +306 -0
- package/server/services/sqlite/dataBrowserService.js +255 -0
- package/server/services/sqlite/exportService.js +34 -0
- package/server/services/sqlite/importService.js +111 -0
- package/server/services/sqlite/introspection.js +302 -0
- package/server/services/sqlite/overviewService.js +109 -0
- package/server/services/sqlite/sqlExecutor.js +434 -0
- package/server/services/sqlite/structureService.js +60 -0
- package/server/services/storage/appStateStore.js +530 -0
- package/server/utils/csv.js +34 -0
- package/server/utils/errors.js +175 -0
- package/server/utils/fileValidation.js +135 -0
- package/server/utils/identifier.js +38 -0
- package/server/utils/sqliteTypes.js +112 -0
- package/styles/base.css +176 -0
- package/styles/components.css +323 -0
- package/styles/layout.css +101 -0
- package/styles/tokens.css +49 -0
- package/styles/views.css +84 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
const {
|
|
2
|
+
NotFoundError,
|
|
3
|
+
ValidationError,
|
|
4
|
+
mapSqliteError,
|
|
5
|
+
} = require("../../utils/errors");
|
|
6
|
+
const { quoteIdentifier } = require("../../utils/identifier");
|
|
7
|
+
const { normalizeDeclaredType } = require("../../utils/sqliteTypes");
|
|
8
|
+
|
|
9
|
+
function getTableListMap(db) {
|
|
10
|
+
try {
|
|
11
|
+
const rows = db.prepare("PRAGMA table_list").all();
|
|
12
|
+
return new Map(
|
|
13
|
+
rows
|
|
14
|
+
.filter((row) => row.schema === "main")
|
|
15
|
+
.map((row) => [row.name, row])
|
|
16
|
+
);
|
|
17
|
+
} catch (error) {
|
|
18
|
+
return new Map();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getMasterEntry(db, type, name) {
|
|
23
|
+
const entry = db
|
|
24
|
+
.prepare(
|
|
25
|
+
"SELECT type, name, tbl_name AS tableName, sql FROM sqlite_master WHERE type = ? AND name = ?"
|
|
26
|
+
)
|
|
27
|
+
.get(type, name);
|
|
28
|
+
|
|
29
|
+
if (!entry) {
|
|
30
|
+
throw new NotFoundError(`${type} not found: ${name}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return entry;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getRawStructureEntries(db) {
|
|
37
|
+
return db
|
|
38
|
+
.prepare(
|
|
39
|
+
[
|
|
40
|
+
"SELECT type, name, tbl_name AS tableName, sql",
|
|
41
|
+
"FROM sqlite_master",
|
|
42
|
+
"WHERE name NOT LIKE 'sqlite_%'",
|
|
43
|
+
"ORDER BY type ASC, name ASC",
|
|
44
|
+
].join(" ")
|
|
45
|
+
)
|
|
46
|
+
.all();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizeColumn(column, visibleSet) {
|
|
50
|
+
const typeMeta = normalizeDeclaredType(column.type);
|
|
51
|
+
const hiddenValue = Number(column.hidden ?? 0);
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
cid: column.cid,
|
|
55
|
+
name: column.name,
|
|
56
|
+
declaredType: typeMeta.declaredType,
|
|
57
|
+
affinity: typeMeta.affinity,
|
|
58
|
+
notNull: Boolean(column.notnull),
|
|
59
|
+
defaultValue: column.dflt_value,
|
|
60
|
+
primaryKeyPosition: Number(column.pk ?? 0),
|
|
61
|
+
hidden: hiddenValue,
|
|
62
|
+
visible: visibleSet.has(column.name),
|
|
63
|
+
generated: hiddenValue === 2 || hiddenValue === 3,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function groupForeignKeys(rows) {
|
|
68
|
+
const grouped = new Map();
|
|
69
|
+
|
|
70
|
+
rows.forEach((row) => {
|
|
71
|
+
if (!grouped.has(row.id)) {
|
|
72
|
+
grouped.set(row.id, {
|
|
73
|
+
id: row.id,
|
|
74
|
+
referencedTable: row.table,
|
|
75
|
+
onUpdate: row.on_update,
|
|
76
|
+
onDelete: row.on_delete,
|
|
77
|
+
match: row.match,
|
|
78
|
+
mappings: [],
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
grouped.get(row.id).mappings.push({
|
|
83
|
+
sequence: row.seq,
|
|
84
|
+
from: row.from,
|
|
85
|
+
to: row.to,
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return Array.from(grouped.values());
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function safeCountRows(db, tableName) {
|
|
93
|
+
try {
|
|
94
|
+
const row = db
|
|
95
|
+
.prepare(`SELECT COUNT(*) AS count FROM ${quoteIdentifier(tableName)}`)
|
|
96
|
+
.get();
|
|
97
|
+
return row?.count ?? 0;
|
|
98
|
+
} catch (error) {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function resolveIdentityStrategy(tableDetail) {
|
|
104
|
+
const primaryKeyColumns = tableDetail.columns
|
|
105
|
+
.filter((column) => column.primaryKeyPosition > 0)
|
|
106
|
+
.sort((left, right) => left.primaryKeyPosition - right.primaryKeyPosition);
|
|
107
|
+
|
|
108
|
+
if (primaryKeyColumns.length > 0) {
|
|
109
|
+
return {
|
|
110
|
+
type: "primaryKey",
|
|
111
|
+
columns: primaryKeyColumns.map((column) => column.name),
|
|
112
|
+
composite: primaryKeyColumns.length > 1,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!tableDetail.withoutRowId) {
|
|
117
|
+
return {
|
|
118
|
+
type: "rowid",
|
|
119
|
+
columns: ["rowid"],
|
|
120
|
+
composite: false,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
type: "none",
|
|
126
|
+
columns: [],
|
|
127
|
+
composite: false,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function getTableDetail(db, tableName, options = {}) {
|
|
132
|
+
const entry = getMasterEntry(db, "table", tableName);
|
|
133
|
+
const tableList = getTableListMap(db);
|
|
134
|
+
const tableListEntry = tableList.get(tableName);
|
|
135
|
+
const tableInfo = db
|
|
136
|
+
.prepare(`PRAGMA table_info(${quoteIdentifier(tableName)})`)
|
|
137
|
+
.all();
|
|
138
|
+
const extendedInfo = db
|
|
139
|
+
.prepare(`PRAGMA table_xinfo(${quoteIdentifier(tableName)})`)
|
|
140
|
+
.all();
|
|
141
|
+
const visibleSet = new Set(tableInfo.map((column) => column.name));
|
|
142
|
+
|
|
143
|
+
const columns = extendedInfo
|
|
144
|
+
.map((column) => normalizeColumn(column, visibleSet))
|
|
145
|
+
.sort((left, right) => left.cid - right.cid);
|
|
146
|
+
|
|
147
|
+
const foreignKeys = groupForeignKeys(
|
|
148
|
+
db.prepare(`PRAGMA foreign_key_list(${quoteIdentifier(tableName)})`).all()
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const indexList = db
|
|
152
|
+
.prepare(`PRAGMA index_list(${quoteIdentifier(tableName)})`)
|
|
153
|
+
.all()
|
|
154
|
+
.map((indexEntry) => {
|
|
155
|
+
let indexColumns = [];
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
indexColumns = db
|
|
159
|
+
.prepare(`PRAGMA index_xinfo(${quoteIdentifier(indexEntry.name)})`)
|
|
160
|
+
.all()
|
|
161
|
+
.filter((row) => row.key === 1)
|
|
162
|
+
.map((row) => ({
|
|
163
|
+
sequence: row.seqno,
|
|
164
|
+
cid: row.cid,
|
|
165
|
+
name: row.name,
|
|
166
|
+
descending: Boolean(row.desc),
|
|
167
|
+
collation: row.coll,
|
|
168
|
+
}));
|
|
169
|
+
} catch (error) {
|
|
170
|
+
indexColumns = db
|
|
171
|
+
.prepare(`PRAGMA index_info(${quoteIdentifier(indexEntry.name)})`)
|
|
172
|
+
.all()
|
|
173
|
+
.map((row) => ({
|
|
174
|
+
sequence: row.seqno,
|
|
175
|
+
cid: row.cid,
|
|
176
|
+
name: row.name,
|
|
177
|
+
}));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
name: indexEntry.name,
|
|
182
|
+
unique: Boolean(indexEntry.unique),
|
|
183
|
+
origin: indexEntry.origin,
|
|
184
|
+
partial: Boolean(indexEntry.partial),
|
|
185
|
+
columns: indexColumns,
|
|
186
|
+
};
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const triggers = db
|
|
190
|
+
.prepare(
|
|
191
|
+
"SELECT name, tbl_name AS tableName, sql FROM sqlite_master WHERE type = 'trigger' AND tbl_name = ? ORDER BY name ASC"
|
|
192
|
+
)
|
|
193
|
+
.all(tableName);
|
|
194
|
+
|
|
195
|
+
const withoutRowId =
|
|
196
|
+
typeof tableListEntry?.wr === "number"
|
|
197
|
+
? Boolean(tableListEntry.wr)
|
|
198
|
+
: /WITHOUT\s+ROWID/i.test(entry.sql || "");
|
|
199
|
+
|
|
200
|
+
const tableDetail = {
|
|
201
|
+
type: entry.type,
|
|
202
|
+
name: entry.name,
|
|
203
|
+
ddl: entry.sql,
|
|
204
|
+
withoutRowId,
|
|
205
|
+
strict: Boolean(tableListEntry?.strict),
|
|
206
|
+
columns,
|
|
207
|
+
foreignKeys,
|
|
208
|
+
indexes: indexList,
|
|
209
|
+
indexCount: indexList.length,
|
|
210
|
+
triggers,
|
|
211
|
+
rowCount: options.includeRowCount === false ? null : safeCountRows(db, tableName),
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
tableDetail.identityStrategy = resolveIdentityStrategy(tableDetail);
|
|
215
|
+
tableDetail.notSafelyUpdatable = tableDetail.identityStrategy.type === "none";
|
|
216
|
+
|
|
217
|
+
return tableDetail;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function getViewDetail(db, viewName) {
|
|
221
|
+
const entry = getMasterEntry(db, "view", viewName);
|
|
222
|
+
let columns = [];
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
columns = db
|
|
226
|
+
.prepare(`SELECT * FROM ${quoteIdentifier(viewName)} LIMIT 0`)
|
|
227
|
+
.columns()
|
|
228
|
+
.map((column) => ({
|
|
229
|
+
name: column.name,
|
|
230
|
+
}));
|
|
231
|
+
} catch (error) {
|
|
232
|
+
columns = [];
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
type: entry.type,
|
|
237
|
+
name: entry.name,
|
|
238
|
+
ddl: entry.sql,
|
|
239
|
+
columns,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function getIndexDetail(db, indexName) {
|
|
244
|
+
const entry = getMasterEntry(db, "index", indexName);
|
|
245
|
+
const indexInfo = db
|
|
246
|
+
.prepare(`PRAGMA index_xinfo(${quoteIdentifier(indexName)})`)
|
|
247
|
+
.all()
|
|
248
|
+
.map((row) => ({
|
|
249
|
+
sequence: row.seqno,
|
|
250
|
+
cid: row.cid,
|
|
251
|
+
name: row.name,
|
|
252
|
+
descending: Boolean(row.desc),
|
|
253
|
+
collation: row.coll,
|
|
254
|
+
key: Boolean(row.key),
|
|
255
|
+
}));
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
type: entry.type,
|
|
259
|
+
name: entry.name,
|
|
260
|
+
tableName: entry.tableName,
|
|
261
|
+
ddl: entry.sql,
|
|
262
|
+
columns: indexInfo,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function listSchema(db) {
|
|
267
|
+
const entries = getRawStructureEntries(db);
|
|
268
|
+
const tables = entries
|
|
269
|
+
.filter((entry) => entry.type === "table")
|
|
270
|
+
.map((entry) => getTableDetail(db, entry.name));
|
|
271
|
+
const views = entries
|
|
272
|
+
.filter((entry) => entry.type === "view")
|
|
273
|
+
.map((entry) => getViewDetail(db, entry.name));
|
|
274
|
+
const indexes = entries
|
|
275
|
+
.filter((entry) => entry.type === "index")
|
|
276
|
+
.map((entry) => getIndexDetail(db, entry.name));
|
|
277
|
+
const triggers = entries
|
|
278
|
+
.filter((entry) => entry.type === "trigger")
|
|
279
|
+
.map((entry) => ({
|
|
280
|
+
type: entry.type,
|
|
281
|
+
name: entry.name,
|
|
282
|
+
tableName: entry.tableName,
|
|
283
|
+
ddl: entry.sql,
|
|
284
|
+
}));
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
tables,
|
|
288
|
+
views,
|
|
289
|
+
indexes,
|
|
290
|
+
triggers,
|
|
291
|
+
masterEntries: entries,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
module.exports = {
|
|
296
|
+
getIndexDetail,
|
|
297
|
+
getRawStructureEntries,
|
|
298
|
+
getTableDetail,
|
|
299
|
+
getViewDetail,
|
|
300
|
+
listSchema,
|
|
301
|
+
resolveIdentityStrategy,
|
|
302
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
const { listSchema } = require("./introspection");
|
|
2
|
+
|
|
3
|
+
class OverviewService {
|
|
4
|
+
constructor({ connectionManager }) {
|
|
5
|
+
this.connectionManager = connectionManager;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
safePragmaValue(db, pragmaName) {
|
|
9
|
+
const row = db.prepare(`PRAGMA ${pragmaName}`).get();
|
|
10
|
+
return row ? Object.values(row)[0] : null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
getDbStatSizes(db) {
|
|
14
|
+
try {
|
|
15
|
+
return db
|
|
16
|
+
.prepare(
|
|
17
|
+
[
|
|
18
|
+
"SELECT name, SUM(pgsize) AS sizeBytes",
|
|
19
|
+
"FROM dbstat",
|
|
20
|
+
"WHERE name NOT LIKE 'sqlite_%'",
|
|
21
|
+
"GROUP BY name",
|
|
22
|
+
"ORDER BY sizeBytes DESC",
|
|
23
|
+
].join(" ")
|
|
24
|
+
)
|
|
25
|
+
.all();
|
|
26
|
+
} catch (error) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
getOverview() {
|
|
32
|
+
const connection = this.connectionManager.getActiveConnection();
|
|
33
|
+
const db = this.connectionManager.getActiveDatabase();
|
|
34
|
+
const schema = listSchema(db);
|
|
35
|
+
const pageSize = this.safePragmaValue(db, "page_size");
|
|
36
|
+
const pageCount = this.safePragmaValue(db, "page_count");
|
|
37
|
+
const freelistCount = this.safePragmaValue(db, "freelist_count");
|
|
38
|
+
const warnings = [];
|
|
39
|
+
|
|
40
|
+
const dbStatSizes = this.getDbStatSizes(db);
|
|
41
|
+
|
|
42
|
+
if (!dbStatSizes) {
|
|
43
|
+
warnings.push("Estimated table sizes unavailable because dbstat is not enabled.");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
connection,
|
|
48
|
+
file: {
|
|
49
|
+
filename: connection.label,
|
|
50
|
+
path: connection.path,
|
|
51
|
+
sizeBytes: connection.sizeBytes,
|
|
52
|
+
lastModifiedAt: connection.lastModifiedAt,
|
|
53
|
+
},
|
|
54
|
+
sqlite: {
|
|
55
|
+
version: db.prepare("SELECT sqlite_version() AS version").get().version,
|
|
56
|
+
pageSize,
|
|
57
|
+
pageCount,
|
|
58
|
+
freelistCount,
|
|
59
|
+
journalMode: this.safePragmaValue(db, "journal_mode"),
|
|
60
|
+
foreignKeys: Boolean(this.safePragmaValue(db, "foreign_keys")),
|
|
61
|
+
autoVacuum: this.safePragmaValue(db, "auto_vacuum"),
|
|
62
|
+
encoding: this.safePragmaValue(db, "encoding"),
|
|
63
|
+
userVersion: this.safePragmaValue(db, "user_version"),
|
|
64
|
+
schemaVersion: this.safePragmaValue(db, "schema_version"),
|
|
65
|
+
integrityCheck: this.safePragmaValue(db, "integrity_check"),
|
|
66
|
+
quickCheck: this.safePragmaValue(db, "quick_check"),
|
|
67
|
+
},
|
|
68
|
+
counts: {
|
|
69
|
+
tables: schema.tables.length,
|
|
70
|
+
views: schema.views.length,
|
|
71
|
+
indexes: schema.indexes.length,
|
|
72
|
+
triggers: schema.triggers.length,
|
|
73
|
+
},
|
|
74
|
+
topTablesByRowCount: [...schema.tables]
|
|
75
|
+
.sort((left, right) => (right.rowCount ?? 0) - (left.rowCount ?? 0))
|
|
76
|
+
.slice(0, 10)
|
|
77
|
+
.map((table) => ({
|
|
78
|
+
name: table.name,
|
|
79
|
+
rowCount: table.rowCount,
|
|
80
|
+
indexCount: table.indexCount,
|
|
81
|
+
})),
|
|
82
|
+
topTablesByEstimatedSize: dbStatSizes
|
|
83
|
+
? dbStatSizes.slice(0, 10).map((row) => ({
|
|
84
|
+
name: row.name,
|
|
85
|
+
sizeBytes: row.sizeBytes,
|
|
86
|
+
}))
|
|
87
|
+
: [],
|
|
88
|
+
estimatedSizeBytes:
|
|
89
|
+
typeof pageSize === "number" && typeof pageCount === "number"
|
|
90
|
+
? pageSize * pageCount
|
|
91
|
+
: null,
|
|
92
|
+
warnings,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
getStatus() {
|
|
97
|
+
const active = this.connectionManager.getActiveConnection();
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
connected: Boolean(active),
|
|
101
|
+
activeConnection: active,
|
|
102
|
+
readOnly: active?.readOnly ?? false,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = {
|
|
108
|
+
OverviewService,
|
|
109
|
+
};
|