sqlite-hub 0.9.13 → 0.11.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.
@@ -16,8 +16,37 @@ function getSelectedRow(state) {
16
16
  return state.dataBrowser.table?.rows?.[rowIndex] ?? null;
17
17
  }
18
18
 
19
+ function getFilteredTables(tables, searchQuery) {
20
+ const normalizedSearch = String(searchQuery ?? '')
21
+ .trim()
22
+ .toLowerCase();
23
+
24
+ if (!normalizedSearch) {
25
+ return tables;
26
+ }
27
+
28
+ return tables.filter(table => table.name.toLowerCase().includes(normalizedSearch));
29
+ }
30
+
31
+ function renderDataTableSearch(state) {
32
+ return `
33
+ <label class="table-designer-sidebar__search">
34
+ <span class="material-symbols-outlined text-sm text-on-surface-variant/55">search</span>
35
+ <input
36
+ class="table-designer-sidebar__search-input"
37
+ data-bind="data-table-search"
38
+ placeholder="Search tables..."
39
+ spellcheck="false"
40
+ type="search"
41
+ value="${escapeHtml(state.dataBrowser.tableSearchQuery ?? '')}"
42
+ />
43
+ </label>
44
+ `;
45
+ }
46
+
19
47
  function renderTableList(state) {
20
48
  const tables = state.dataBrowser.tables ?? [];
49
+ const filteredTables = getFilteredTables(tables, state.dataBrowser.tableSearchQuery);
21
50
  const activeName = state.dataBrowser.selectedTable;
22
51
 
23
52
  if (state.dataBrowser.loading && !tables.length) {
@@ -39,10 +68,18 @@ function renderTableList(state) {
39
68
  `;
40
69
  }
41
70
 
71
+ if (!filteredTables.length) {
72
+ return `
73
+ <div class="px-6 py-6 text-sm text-on-surface-variant/55">
74
+ No tables match the current search.
75
+ </div>
76
+ `;
77
+ }
78
+
42
79
  return `
43
80
  <div class="custom-scrollbar flex-1 overflow-auto px-4 py-4">
44
81
  <div class="space-y-2">
45
- ${tables
82
+ ${filteredTables
46
83
  .map(
47
84
  table => `
48
85
  <button
@@ -203,71 +240,88 @@ function renderSortableHeader(columnName, sortColumn, sortDirection, action) {
203
240
  `;
204
241
  }
205
242
 
206
- function getFilteredTableRows(table, state) {
207
- const allRows = table?.rows ?? [];
243
+ function getActiveFilterColumn(table, state) {
208
244
  const availableColumns = table?.columns ?? [];
209
- const searchQuery = String(state.dataBrowser.searchQuery ?? '')
210
- .trim()
211
- .toLowerCase();
212
- const activeColumn = availableColumns.includes(state.dataBrowser.searchColumn)
245
+
246
+ return availableColumns.includes(state.dataBrowser.searchColumn)
213
247
  ? state.dataBrowser.searchColumn
214
248
  : (availableColumns[0] ?? '');
249
+ }
215
250
 
216
- const indexedRows = allRows.map((row, index) => ({
217
- row,
218
- index,
219
- }));
251
+ function isTextFilterColumn(table, columnName) {
252
+ const column = (table?.columnMeta ?? []).find(item => item.name === columnName);
253
+
254
+ return String(column?.affinity ?? '').toUpperCase() === 'TEXT';
255
+ }
220
256
 
221
- if (!searchQuery || !activeColumn) {
222
- return {
223
- activeColumn,
224
- filteredRows: indexedRows,
225
- searchQuery,
226
- };
257
+ function getFilterOperatorLabel(operator, textColumn) {
258
+ if (textColumn && operator === '=') {
259
+ return 'contains';
227
260
  }
228
261
 
229
- return {
230
- activeColumn,
231
- searchQuery,
232
- filteredRows: indexedRows.filter(({ row }) =>
233
- formatCellValue(row[activeColumn]).toLowerCase().includes(searchQuery),
234
- ),
235
- };
262
+ if (textColumn && operator === 'equals') {
263
+ return 'equals';
264
+ }
265
+
266
+ if (textColumn && operator === '!=') {
267
+ return 'not contains';
268
+ }
269
+
270
+ return operator;
236
271
  }
237
272
 
238
- function renderTableSearchBar(table, state, activeColumn, filteredRowCount) {
273
+ function renderTableFilterBar(table, state, activeColumn) {
239
274
  const columns = table?.columns ?? [];
240
275
 
241
276
  if (!table || !columns.length) {
242
277
  return '';
243
278
  }
279
+
244
280
  const columnOptions = columns
245
- .map(columnName => [
246
- '<option value="',
247
- escapeHtml(columnName),
248
- '" ',
249
- columnName === activeColumn ? 'selected' : '',
250
- '>',
251
- escapeHtml(columnName),
252
- '</option>',
253
- ].join(''))
281
+ .map(columnName =>
282
+ [
283
+ '<option value="',
284
+ escapeHtml(columnName),
285
+ '" ',
286
+ columnName === activeColumn ? 'selected' : '',
287
+ '>',
288
+ escapeHtml(columnName),
289
+ '</option>',
290
+ ].join(''),
291
+ )
254
292
  .join('');
293
+ const activeColumnIsText = isTextFilterColumn(table, activeColumn);
294
+ const operators = activeColumnIsText ? ['=', 'equals', '!=', '<', '>', '<=', '>='] : ['=', '!=', '<', '>', '<=', '>='];
295
+ const activeOperator = operators.includes(state.dataBrowser.filterOperator) ? state.dataBrowser.filterOperator : '=';
296
+ const operatorOptions = operators
297
+ .map(operator =>
298
+ [
299
+ '<option value="',
300
+ escapeHtml(operator),
301
+ '" ',
302
+ operator === activeOperator ? 'selected' : '',
303
+ '>',
304
+ escapeHtml(getFilterOperatorLabel(operator, activeColumnIsText)),
305
+ '</option>',
306
+ ].join(''),
307
+ )
308
+ .join('');
309
+ const filterValue = String(state.dataBrowser.searchQuery ?? '');
255
310
 
256
311
  return [
257
312
  '<div class="flex flex-wrap items-center gap-3 border-b border-outline-variant/10 bg-surface-container-low px-6 py-4">',
258
- '<label class="control-shell flex min-w-[18rem] flex-1 items-center gap-3 border border-outline-variant/20 bg-surface-container-lowest px-3">',
259
- '<span class="material-symbols-outlined text-base text-on-surface-variant/55">search</span>',
260
- '<input class="control-input control-input--ghost min-w-0 flex-1 text-sm text-on-surface outline-none placeholder:text-on-surface-variant/40" data-bind="data-search-query" placeholder="Filter current page..." type="search" value="',
261
- escapeHtml(state.dataBrowser.searchQuery ?? ''),
262
- '" /></label>',
313
+ '<span class="material-symbols-outlined text-base text-on-surface-variant/55">filter_alt</span>',
263
314
  '<select class="control-select min-w-[14rem] border border-outline-variant/20 bg-surface-container-lowest font-mono text-xs tracking-[0.04em] text-on-surface outline-none" data-bind="data-search-column">',
264
315
  columnOptions,
265
316
  '</select>',
266
- '<div class="text-[10px] font-mono tracking-[0.14em] text-on-surface-variant/55">',
267
- escapeHtml(formatNumber(filteredRowCount)),
268
- ' match',
269
- filteredRowCount === 1 ? '' : 'es',
270
- ' on this page</div></div>',
317
+ '<select class="control-select min-w-[7rem] border border-outline-variant/20 bg-surface-container-lowest font-mono text-xs tracking-[0.04em] text-on-surface outline-none" data-bind="data-filter-operator">',
318
+ operatorOptions,
319
+ '</select>',
320
+ '<label class="control-shell flex min-w-[18rem] flex-1 items-center gap-3 border border-outline-variant/20 bg-surface-container-lowest px-3">',
321
+ '<input class="control-input control-input--ghost min-w-0 flex-1 text-sm text-on-surface outline-none placeholder:text-on-surface-variant/40" data-bind="data-search-query" placeholder="Value..." type="search" value="',
322
+ escapeHtml(filterValue),
323
+ '" /></label>',
324
+ '</div>',
271
325
  ].join('');
272
326
  }
273
327
 
@@ -300,7 +354,11 @@ function renderTableSurface(state) {
300
354
  `;
301
355
  }
302
356
 
303
- const { activeColumn, filteredRows, searchQuery } = getFilteredTableRows(table, state);
357
+ const activeColumn = getActiveFilterColumn(table, state);
358
+ const indexedRows = (table.rows ?? []).map((row, index) => ({
359
+ row,
360
+ index,
361
+ }));
304
362
  const sortColumn = state.dataBrowser.sortColumn;
305
363
  const sortDirection = state.dataBrowser.sortDirection;
306
364
  const columns = (table.columns ?? []).map(columnName => ({
@@ -324,44 +382,41 @@ function renderTableSurface(state) {
324
382
  const pageCount = table.pageCount ?? Math.max(1, Math.ceil(totalRows / (table.limit ?? 50)));
325
383
  const fromRow = totalRows === 0 ? 0 : (table.offset ?? 0) + 1;
326
384
  const toRow = totalRows === 0 ? 0 : Math.min((table.offset ?? 0) + (table.rows?.length ?? 0), totalRows);
327
- const pageSizes = [25, 50, 100];
328
- const filteredRowCount = filteredRows.length;
329
- const hasActiveSearch = Boolean(searchQuery);
385
+ const pageSizes = Object.freeze([25, 50, 100, 250]);
386
+ const hasActiveFilter = Boolean(String(state.dataBrowser.searchQuery ?? '').trim() && activeColumn);
330
387
  const gridMarkup = renderDataGrid({
331
388
  columns,
332
- rows: filteredRows.map(({ row }) => row),
389
+ rows: indexedRows.map(({ row }) => row),
333
390
  tableClass: 'min-w-full border-collapse text-left font-mono text-xs',
334
391
  theadClass: 'sticky top-0 z-10 bg-surface-container-highest',
335
392
  tbodyClass: 'divide-y divide-outline-variant/5',
336
- getRowClass: (_, filteredIndex) => {
337
- const rowIndex = filteredRows[filteredIndex]?.index ?? filteredIndex;
393
+ getRowClass: (_, rowIndexOnPage) => {
394
+ const rowIndex = indexedRows[rowIndexOnPage]?.index ?? rowIndexOnPage;
338
395
 
339
396
  return [
340
397
  'data-browser-row',
341
- filteredIndex % 2 === 0 ? 'data-browser-row--even' : 'data-browser-row--odd',
398
+ rowIndexOnPage % 2 === 0 ? 'data-browser-row--even' : 'data-browser-row--odd',
342
399
  state.dataBrowser.selectedRowIndex === rowIndex ? 'is-selected' : '',
343
400
  'cursor-pointer transition-colors',
344
- ].filter(Boolean).join(' ');
401
+ ]
402
+ .filter(Boolean)
403
+ .join(' ');
345
404
  },
346
- getRowAttrs: (_, filteredIndex) => {
347
- const rowIndex = filteredRows[filteredIndex]?.index ?? filteredIndex;
405
+ getRowAttrs: (_, rowIndexOnPage) => {
406
+ const rowIndex = indexedRows[rowIndexOnPage]?.index ?? rowIndexOnPage;
348
407
 
349
408
  return ['data-action="select-data-row" data-row-index="', rowIndex, '"'].join('');
350
409
  },
351
410
  });
352
411
  const emptyMarkup = !table.rows?.length
353
- ? '<div class="flex min-h-[180px] items-center justify-center border-t border-outline-variant/10"><p class="font-mono text-[10px] uppercase tracking-[0.22em] text-on-surface-variant/40">TABLE_IS_EMPTY</p></div>'
354
- : !filteredRowCount
355
- ? [
356
- '<div class="flex min-h-[180px] items-center justify-center border-t border-outline-variant/10">',
357
- '<p class="font-mono text-[10px] tracking-[0.18em] text-on-surface-variant/40">',
358
- hasActiveSearch ? 'No matching rows on this page.' : 'No rows available.',
359
- '</p></div>',
360
- ].join('')
361
- : '';
362
- const visibleRowsText = hasActiveSearch
363
- ? [' // ', escapeHtml(formatNumber(filteredRowCount)), ' visible on this page'].join('')
412
+ ? [
413
+ '<div class="flex min-h-[180px] items-center justify-center border-t border-outline-variant/10">',
414
+ '<p class="font-mono text-[10px] uppercase tracking-[0.22em] text-on-surface-variant/40">',
415
+ hasActiveFilter ? 'NO_ROWS_MATCH_FILTER' : 'TABLE_IS_EMPTY',
416
+ '</p></div>',
417
+ ].join('')
364
418
  : '';
419
+ const filteredRowsText = hasActiveFilter ? ' filtered' : '';
365
420
  const pageSizeButtons = pageSizes
366
421
  .map(pageSize =>
367
422
  [
@@ -378,7 +433,7 @@ function renderTableSurface(state) {
378
433
 
379
434
  return [
380
435
  '<div class="flex flex-1 min-h-0 flex-col bg-surface-container-lowest">',
381
- renderTableSearchBar(table, state, activeColumn, filteredRowCount),
436
+ renderTableFilterBar(table, state, activeColumn),
382
437
  '<div class="custom-scrollbar flex-1 overflow-auto">',
383
438
  gridMarkup,
384
439
  emptyMarkup,
@@ -390,8 +445,8 @@ function renderTableSurface(state) {
390
445
  escapeHtml(formatNumber(toRow)),
391
446
  ' of ',
392
447
  escapeHtml(formatNumber(totalRows)),
448
+ filteredRowsText,
393
449
  ' rows',
394
- visibleRowsText,
395
450
  '</div>',
396
451
  '<div class="flex flex-wrap items-center gap-4"><div class="flex items-center gap-2">',
397
452
  '<span class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">rows</span>',
@@ -433,7 +488,10 @@ export function renderDataRowEditorPanel(state) {
433
488
  (foreignKey.mappings ?? []).map(mapping => String(mapping.from ?? '').trim()).filter(Boolean),
434
489
  ),
435
490
  );
436
- const getColumnTypeBadge = column => String(column.declaredType || column.affinity || 'BLOB').trim().toUpperCase();
491
+ const getColumnTypeBadge = column =>
492
+ String(column.declaredType || column.affinity || 'BLOB')
493
+ .trim()
494
+ .toUpperCase();
437
495
  const getColumnNumberInputMeta = column => {
438
496
  const affinity = String(column.affinity ?? '').toUpperCase();
439
497
 
@@ -556,6 +614,7 @@ export function renderDataView(state) {
556
614
  total ${escapeHtml(formatNumber(state.dataBrowser.tables.length))}
557
615
  </div>
558
616
  </div>
617
+ ${renderDataTableSearch(state)}
559
618
  ${renderTableList(state)}
560
619
  </aside>
561
620
  `
@@ -4,7 +4,7 @@ import { renderQueryHistoryDetail } from '../components/queryHistoryDetail.js';
4
4
  import { renderQueryHistoryPanel } from '../components/queryHistoryPanel.js';
5
5
  import { renderRowEditorPanel } from '../components/rowEditorPanel.js';
6
6
  import { renderQueryResultsPane } from '../components/queryResults.js';
7
- import { getCurrentConnection, getQueryMessages, getQueryPerformance } from '../store.js';
7
+ import { getQueryMessages, getQueryPerformance } from '../store.js';
8
8
  import { escapeHtml, formatBytes, formatCellValue, formatExecutionTimeMs, formatNumber, isBlobPreview } from '../utils/format.js';
9
9
 
10
10
  function renderMissingDatabase() {
@@ -244,7 +244,6 @@ function renderResultsSurface(state, isResultsRoute) {
244
244
  }
245
245
 
246
246
  export function renderEditorView(state, { isResultsRoute = false } = {}) {
247
- const connection = getCurrentConnection(state);
248
247
  const editorSectionClass = state.editor.editorPanelVisible ? 'min-h-[27.5%] max-h-[60%]' : 'flex-none';
249
248
  const resultsSectionClass = 'flex-1';
250
249
 
@@ -261,7 +260,6 @@ export function renderEditorView(state, { isResultsRoute = false } = {}) {
261
260
  historyTotal: state.editor.historyTotal,
262
261
  editorVisible: state.editor.editorPanelVisible,
263
262
  historyVisible: state.editor.historyPanelVisible,
264
- title: connection?.label ?? 'SQLite Query Workspace',
265
263
  })}
266
264
  </section>
267
265
  <section class="${resultsSectionClass} flex min-h-0 min-w-0 flex-col overflow-hidden">
@@ -2,7 +2,7 @@ import { clearInspector, renderDdlSection, renderInspector } from "../components
2
2
  import { escapeHtml, formatNumber } from "../utils/format.js";
3
3
 
4
4
  function renderEntryGroup(title, entries, activeName, options = {}) {
5
- const { compact = false, showMeta = true } = options;
5
+ const { compact = false, showMeta = true, emptyMessage = null } = options;
6
6
  const entriesMarkup = entries.length
7
7
  ? [
8
8
  '<div class="space-y-2">',
@@ -51,8 +51,8 @@ function renderEntryGroup(title, entries, activeName, options = {}) {
51
51
  ].join("")
52
52
  : [
53
53
  '<div class="text-sm text-on-surface-variant/45">No ',
54
- escapeHtml(title.toLowerCase()),
55
- " found.</div>",
54
+ emptyMessage ? escapeHtml(emptyMessage) : `${escapeHtml(title.toLowerCase())} found.`,
55
+ "</div>",
56
56
  ].join("");
57
57
 
58
58
  return `
@@ -65,6 +65,34 @@ function renderEntryGroup(title, entries, activeName, options = {}) {
65
65
  `;
66
66
  }
67
67
 
68
+ function getFilteredTables(tables, searchQuery) {
69
+ const normalizedSearch = String(searchQuery ?? "")
70
+ .trim()
71
+ .toLowerCase();
72
+
73
+ if (!normalizedSearch) {
74
+ return tables;
75
+ }
76
+
77
+ return tables.filter((table) => table.name.toLowerCase().includes(normalizedSearch));
78
+ }
79
+
80
+ function renderStructureTableSearch(state) {
81
+ return `
82
+ <label class="table-designer-sidebar__search">
83
+ <span class="material-symbols-outlined text-sm text-on-surface-variant/55">search</span>
84
+ <input
85
+ class="table-designer-sidebar__search-input"
86
+ data-bind="structure-table-search"
87
+ placeholder="Search tables..."
88
+ spellcheck="false"
89
+ type="search"
90
+ value="${escapeHtml(state.structure.tableSearchQuery ?? "")}"
91
+ />
92
+ </label>
93
+ `;
94
+ }
95
+
68
96
  function renderLoadingInspector() {
69
97
  return `
70
98
  <div class="structure-graph__panel is-empty">
@@ -308,6 +336,9 @@ function renderStructureWorkspaceHeader(structure, selectedName) {
308
336
 
309
337
  export function renderStructureView(state) {
310
338
  const structure = state.structure.data;
339
+ const filteredTables = structure
340
+ ? getFilteredTables(structure.grouped.tables ?? [], state.structure.tableSearchQuery)
341
+ : [];
311
342
  const detail =
312
343
  state.structure.detail?.name === state.structure.selectedName ? state.structure.detail : null;
313
344
  const tablesVisible = state.structure.tablesVisible !== false;
@@ -334,15 +365,22 @@ export function renderStructureView(state) {
334
365
  )}
335
366
  </div>
336
367
  </div>
368
+ ${renderStructureTableSearch(state)}
337
369
  <div class="structure-view__sidebar-body custom-scrollbar">
338
370
  ${
339
371
  structure
340
372
  ? `
341
373
  ${renderEntryGroup(
342
374
  "Tables",
343
- structure.grouped.tables,
375
+ filteredTables,
344
376
  state.structure.selectedName,
345
- { compact: true, showMeta: false }
377
+ {
378
+ compact: true,
379
+ showMeta: false,
380
+ emptyMessage: state.structure.tableSearchQuery
381
+ ? "tables match the current search."
382
+ : null,
383
+ }
346
384
  )}
347
385
  ${renderEntryGroup(
348
386
  "Views",