From e303e9295be4e0e731d191093006592814f449ee Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Tue, 2 Jul 2024 15:38:48 +0700 Subject: [PATCH] feat: group fts5 table --- src/components/gui/schema-sidebar-list.tsx | 57 ++++++++++++++++++++-- src/drivers/base-driver.ts | 6 +++ src/drivers/sqlite/sql-parse-table.test.ts | 27 ++++++++++ src/drivers/sqlite/sql-parse-table.ts | 52 ++++++++++++++++++++ src/drivers/sqljs-driver.ts | 3 +- 5 files changed, 138 insertions(+), 7 deletions(-) diff --git a/src/components/gui/schema-sidebar-list.tsx b/src/components/gui/schema-sidebar-list.tsx index 45336efe..a930bb19 100644 --- a/src/components/gui/schema-sidebar-list.tsx +++ b/src/components/gui/schema-sidebar-list.tsx @@ -15,9 +15,13 @@ interface SchemaListProps { search: string; } +type DatabaseSchemaItemWithIndentation = DatabaseSchemaItem & { + indentation?: number; +}; + type DatabaseSchemaTreeNode = { - node: DatabaseSchemaItem; - sub: DatabaseSchemaItem[]; + node: DatabaseSchemaItemWithIndentation; + sub: DatabaseSchemaItemWithIndentation[]; }; interface SchemaViewItemProps { @@ -30,6 +34,7 @@ interface SchemaViewItemProps { onClick: () => void; onContextMenu: React.MouseEventHandler; indentation?: boolean; + badge?: string; } function SchemaViewItem({ @@ -42,6 +47,7 @@ function SchemaViewItem({ onContextMenu, indentation, item, + badge, }: Readonly) { const regex = new RegExp( "(" + (highlight ?? "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + ")", @@ -83,6 +89,7 @@ function SchemaViewItem({
)} + {splitedText.map((text, idx) => { return text.toLowerCase() === (highlight ?? "").toLowerCase() ? ( @@ -93,6 +100,12 @@ function SchemaViewItem({ {text} ); })} + + {badge && ( + + {badge} + + )} ); @@ -151,11 +164,42 @@ export default function SchemaList({ search }: Readonly) { let tree: DatabaseSchemaTreeNode[] = []; const treeHash: Record = {}; + const excludeTables = new Set(); + const ftsTables: string[] = []; + const ftsSuffix = ["_config", "_content", "_data", "_docsize", "_idx"]; + + // Scan for FTS5 + for (const item of schema) { + if (item.name && item.tableSchema?.fts5) { + const tableName = item.name; + ftsTables.push(tableName); + for (const suffix of ftsSuffix) { + excludeTables.add(tableName + suffix); + } + } + } + for (const item of schema) { if (item.type === "table" || item.type === "view") { const node = { node: item, sub: [] }; treeHash[item.name] = node; - tree.push(node); + + if (item.name && !excludeTables.has(item.name)) { + tree.push(node); + } + } + } + + // Grouping FTS5 table + for (const ftsTableName of ftsTables) { + const ftsSubgroup = treeHash[ftsTableName].sub; + if (ftsSubgroup) { + for (const suffix of ftsSuffix) { + const ftsSubTable = treeHash[ftsTableName + suffix]; + if (ftsSubTable) { + treeHash[ftsTableName].sub.push(ftsSubTable.node); + } + } } } @@ -177,7 +221,9 @@ export default function SchemaList({ search }: Readonly) { return foundName || foundInChildren; }); - return tree.map((r) => [r.node, ...r.sub]).flat(); + return tree + .map((r) => [r.node, ...r.sub.map((d) => ({ ...d, indentation: 1 }))]) + .flat(); }, [schema, search]); return ( @@ -217,9 +263,10 @@ export default function SchemaList({ search }: Readonly) { title={item.name} iconClassName={iconClassName} icon={icon} - indentation={item.type === "trigger"} + indentation={!!item.indentation} selected={schemaIndex === selectedIndex} onClick={() => setSelectedIndex(schemaIndex)} + badge={item.tableSchema?.fts5 ? "fts5" : undefined} /> ); })} diff --git a/src/drivers/base-driver.ts b/src/drivers/base-driver.ts index 74449fe8..803e9f8c 100644 --- a/src/drivers/base-driver.ts +++ b/src/drivers/base-driver.ts @@ -125,6 +125,11 @@ export interface DatabaseTableColumnConstraint { foreignKey?: DatabaseForeignKeyClause; } +export interface DatabaseTableFts5 { + content?: string; + contentRowId?: string; +} + export interface DatabaseTableSchema { columns: DatabaseTableColumn[]; pk: string[]; @@ -132,6 +137,7 @@ export interface DatabaseTableSchema { tableName?: string; constraints?: DatabaseTableColumnConstraint[]; createScript?: string; + fts5?: DatabaseTableFts5; } export type TriggerWhen = "BEFORE" | "AFTER" | "INSTEAD_OF"; diff --git a/src/drivers/sqlite/sql-parse-table.test.ts b/src/drivers/sqlite/sql-parse-table.test.ts index 8aca3e31..a7200dab 100644 --- a/src/drivers/sqlite/sql-parse-table.test.ts +++ b/src/drivers/sqlite/sql-parse-table.test.ts @@ -205,3 +205,30 @@ it("parse create table with table constraints", () => { ], } as DatabaseTableSchema); }); + +it("parse fts5 virtual table", () => { + const sql = `create virtual table name_fts using fts5(name, tokenize='trigram');`; + expect(p(sql)).toEqual({ + tableName: "name_fts", + autoIncrement: false, + pk: [], + columns: [], + constraints: [], + fts5: {}, + } as DatabaseTableSchema); +}); + +it("parse fts5 virtual table with external content", () => { + const sql = `create virtual table name_fts using fts5(name, tokenize='trigram', content='student', content_rowid='id');`; + expect(p(sql)).toEqual({ + tableName: "name_fts", + autoIncrement: false, + pk: [], + columns: [], + constraints: [], + fts5: { + content: "'student'", + contentRowId: "'id'", + }, + } as DatabaseTableSchema); +}); diff --git a/src/drivers/sqlite/sql-parse-table.ts b/src/drivers/sqlite/sql-parse-table.ts index 864b7695..0ded51cf 100644 --- a/src/drivers/sqlite/sql-parse-table.ts +++ b/src/drivers/sqlite/sql-parse-table.ts @@ -5,6 +5,7 @@ import type { DatabaseTableColumnConstraint, DatabaseTableSchema, SqlOrder, + DatabaseTableFts5, } from "@/drivers/base-driver"; import { unescapeIdentity } from "./sql-helper"; import { sqliteDialect } from "@/drivers/sqlite/sqlite-dialect"; @@ -456,6 +457,43 @@ function parseTableDefinition(cursor: Cursor): { return { columns, constraints }; } +function parseFTS5(cursor: Cursor | null): DatabaseTableFts5 { + if (!cursor) return {}; + + let content: string | undefined; + let contentRowId: string | undefined; + + const ptr = cursor; + while (!ptr.end()) { + if (ptr.match("content")) { + ptr.next(); + if (ptr.match("=")) { + ptr.next(); + if (!ptr.end()) { + content = unescapeIdentity(ptr.read()); + ptr.next(); + } + } + } else if (ptr.match("content_rowid")) { + ptr.next(); + if (ptr.match("=")) { + ptr.next(); + if (!ptr.end()) { + contentRowId = unescapeIdentity(ptr.read()); + ptr.next(); + } + } + } + + ptr.next(); + } + + return { + content, + contentRowId, + }; +} + // Our parser follows this spec // https://www.sqlite.org/lang_createtable.html export function parseCreateTableScript(sql: string): DatabaseTableSchema { @@ -469,10 +507,23 @@ export function parseCreateTableScript(sql: string): DatabaseTableSchema { cursor.expectKeyword("CREATE"); cursor.expectKeywordOptional("TEMP"); cursor.expectKeywordOptional("TEMPORARY"); + cursor.expectKeywordOptional("VIRTUAL"); cursor.expectKeyword("TABLE"); cursor.expectKeywordsOptional(["IF", "NOT", "EXIST"]); const tableName = cursor.consumeIdentifier(); + // Check for FTS5 + let fts5: DatabaseTableFts5 | undefined; + + if (cursor.match("USING")) { + cursor.next(); + if (cursor.match("FTS5")) { + cursor.next(); + fts5 = parseFTS5(cursor.enterParens()); + cursor.next(); + } + } + const defCursor = cursor.enterParens(); const defs = defCursor ? parseTableDefinition(defCursor) @@ -489,5 +540,6 @@ export function parseCreateTableScript(sql: string): DatabaseTableSchema { ...defs, pk, autoIncrement, + fts5, }; } diff --git a/src/drivers/sqljs-driver.ts b/src/drivers/sqljs-driver.ts index fa013558..379b876b 100644 --- a/src/drivers/sqljs-driver.ts +++ b/src/drivers/sqljs-driver.ts @@ -39,6 +39,7 @@ export default class SqljsDriver extends SqliteLikeBaseDriver { const startTime = Date.now(); const s = this.db.prepare(sql, bind); + const endTime = Date.now(); // Do the transform result here const headerName = s.getColumnNames(); @@ -71,8 +72,6 @@ export default class SqljsDriver extends SqliteLikeBaseDriver { ); } - const endTime = Date.now(); - return { headers, rows,