From 47aaf6c1f436f4e9094a891c7fb86e4d259dc442 Mon Sep 17 00:00:00 2001 From: Kacper Kula Date: Thu, 14 Aug 2025 20:03:53 +0100 Subject: [PATCH 1/3] feat: added support for .sql and .sqlseal files --- src/modules/explorer/Editor.ts | 17 ++ .../explorer/FileDatabaseExplorerView.ts | 105 --------- src/modules/explorer/InitFactory.ts | 12 +- src/modules/explorer/SQLSealFileView.ts | 217 ++++++++++++++++++ src/modules/globalTables/FileConfig.ts | 26 +++ src/modules/globalTables/GlobalTablesView.ts | 2 +- 6 files changed, 267 insertions(+), 112 deletions(-) delete mode 100644 src/modules/explorer/FileDatabaseExplorerView.ts create mode 100644 src/modules/explorer/SQLSealFileView.ts diff --git a/src/modules/explorer/Editor.ts b/src/modules/explorer/Editor.ts index 4365f37..7c4ccae 100644 --- a/src/modules/explorer/Editor.ts +++ b/src/modules/explorer/Editor.ts @@ -129,4 +129,21 @@ export class Editor { // } } } + + getCurrentQuery(): string { + return this.editor?.state.doc.toString() || this.query; + } + + setQuery(newQuery: string) { + this.query = newQuery; + if (this.editor) { + this.editor.dispatch({ + changes: { + from: 0, + to: this.editor.state.doc.length, + insert: newQuery + } + }); + } + } } diff --git a/src/modules/explorer/FileDatabaseExplorerView.ts b/src/modules/explorer/FileDatabaseExplorerView.ts deleted file mode 100644 index cb2c774..0000000 --- a/src/modules/explorer/FileDatabaseExplorerView.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { FileView, IconName, MarkdownPostProcessorContext, Menu, TextFileView, TFile, WorkspaceLeaf } from "obsidian"; -import { GridApi } from "ag-grid-community"; -import { MemoryDatabase } from "./database/memoryDatabase"; -import { DatabaseManager } from "./database/databaseManager"; -import { TableInfo } from "./schemaVisualiser/TableVisualiser"; -import { SchemaVisualiser } from "./schemaVisualiser/SchemaVisualiser"; -import { ExplorerView } from "./explorer/ExplorerView"; -import { Editor } from "./Editor"; -import { ViewPluginGeneratorType } from "../syntaxHighlight/viewPluginGenerator"; -import { CodeblockProcessor } from "../editor/codeblockHandler/CodeblockProcessor"; -import { RendererRegistry } from "../editor/renderer/rendererRegistry"; -import { ModernCellParser } from "../syntaxHighlight/cellParser/ModernCellParser"; -import { Settings } from "../settings/Settings"; -import { Sync } from "../sync/sync/sync"; - -export const FILE_DATABASE_VIEW = 'sqlseal-sqlite-file-view' - -const INITIAL_QUERY = "SELECT name\nFROM sqlite_master\nWHERE type='table'" - -export class FileDatabaseExplorerView extends FileView { - constructor( - leaf: WorkspaceLeaf, - private manager: DatabaseManager, - private viewPluginGenerator: ViewPluginGeneratorType, - private rendererRegistry: RendererRegistry, - private cellParser: ModernCellParser, - private settings: Settings, - private sync: Sync, - - ) { - super(leaf) - } - getViewType(): string { - return FILE_DATABASE_VIEW - } - getDisplayText(): string { - return this.file?.basename || 'Database' - } - - async onOpen() { - } - - onPaneMenu(menu: Menu, source: "more-options" | "tab-header" | string): void { - menu.addItem(i => i.setTitle('test')) - } - - db: MemoryDatabase - async onLoadFile(file: TFile): Promise { - const db = await this.manager.getDatabaseConnection(file) - await db.connect() - this.db = db - - // GETTING ALL TABLES - this.schema = db.getSchema() - this.render() - } - - schema: TableInfo[] - render() { - const codeblockProcessorGenerator = async (el: HTMLElement, source: string) => { - const ctx: MarkdownPostProcessorContext = { - docId: "", - sourcePath: "", - frontmatter: {}, - } as any; - - const processor = new CodeblockProcessor( - el, - source, - ctx, - this.rendererRegistry, - this.db as any, // FIXME - this.cellParser, - this.settings, - this.app, - this.sync, - ); - await processor.onload(); - - // Resizing and layout configuration for explorer - const renderer = processor.renderer - if ('communicator' in renderer && 'gridApi' in (renderer as any)['communicator']) { - const api: GridApi = (renderer.communicator as any).gridApi - api.setGridOption('paginationAutoPageSize', true) - api.setGridOption('domLayout', 'normal') // Override autoHeight for proper pagination - } - - await processor.render(); - return processor - } - - const editor = new Editor(codeblockProcessorGenerator, this.viewPluginGenerator,this.app, INITIAL_QUERY, this.db) - - editor.render(this.contentEl) - - // const vis = new SchemaVisualiser(this.schema) - // vis.show(container) - - } - - getIcon(): IconName { - return 'database' - } - -} \ No newline at end of file diff --git a/src/modules/explorer/InitFactory.ts b/src/modules/explorer/InitFactory.ts index d75dcb8..6111eec 100644 --- a/src/modules/explorer/InitFactory.ts +++ b/src/modules/explorer/InitFactory.ts @@ -9,7 +9,7 @@ import { Settings } from "../settings/Settings"; import { ExplorerView } from "./explorer/ExplorerView"; import { ViewPlugin } from "@codemirror/view"; import { ViewPluginGeneratorType } from "../syntaxHighlight/viewPluginGenerator"; -import { FILE_DATABASE_VIEW, FileDatabaseExplorerView } from "./FileDatabaseExplorerView"; +import { SQLSEAL_FILE_VIEW, SQLSealFileView } from "./SQLSealFileView"; import { DatabaseManager } from "./database/databaseManager"; import { activateView } from "./activateView"; @@ -59,13 +59,13 @@ export class InitFactory { activateView(plugin.app, "sqlseal-explorer-view"), ); - - // Register for extenion - plugin.registerView(FILE_DATABASE_VIEW, (leaf) => { - return new FileDatabaseExplorerView(leaf, dbManager, viewPluginGenerator, rendererRegistry, cellParser, settings, sync) + // Register unified SQLSeal file view for both SQL and database files + plugin.registerView(SQLSEAL_FILE_VIEW, (leaf) => { + return new SQLSealFileView(leaf, dbManager, viewPluginGenerator, rendererRegistry, cellParser, settings, sync, db) }) - plugin.registerExtensions(['sqlite'], FILE_DATABASE_VIEW) + // Register extensions for SQLSeal file view + plugin.registerExtensions(['sql', 'sqlseal', 'sqlite', 'db'], SQLSEAL_FILE_VIEW) plugin.addCommand({ id: 'sqlseal-command-explorer', diff --git a/src/modules/explorer/SQLSealFileView.ts b/src/modules/explorer/SQLSealFileView.ts new file mode 100644 index 0000000..86782f5 --- /dev/null +++ b/src/modules/explorer/SQLSealFileView.ts @@ -0,0 +1,217 @@ +import { FileView, IconName, MarkdownPostProcessorContext, Menu, TextFileView, TFile, WorkspaceLeaf } from "obsidian"; +import { GridApi } from "ag-grid-community"; +import { MemoryDatabase } from "./database/memoryDatabase"; +import { DatabaseManager } from "./database/databaseManager"; +import { TableInfo } from "./schemaVisualiser/TableVisualiser"; +import { Editor } from "./Editor"; +import { ViewPluginGeneratorType } from "../syntaxHighlight/viewPluginGenerator"; +import { CodeblockProcessor } from "../editor/codeblockHandler/CodeblockProcessor"; +import { RendererRegistry } from "../editor/renderer/rendererRegistry"; +import { ModernCellParser } from "../syntaxHighlight/cellParser/ModernCellParser"; +import { Settings } from "../settings/Settings"; +import { Sync } from "../sync/sync/sync"; +import { SqlSealDatabase } from "../database/database"; + +export const SQLSEAL_FILE_VIEW = 'sqlseal-file-view'; + +const DEFAULT_SQLITE_QUERY = "SELECT name\nFROM sqlite_master\nWHERE type='table'"; +const DEFAULT_SQL_QUERY = "SELECT *\nFROM files\nLIMIT 10"; + +export class SQLSealFileView extends TextFileView { + private fileDb: MemoryDatabase | null = null; + private schema: TableInfo[] = []; + private editor: Editor | null = null; + private fileContent: string = ""; + + constructor( + leaf: WorkspaceLeaf, + private manager: DatabaseManager, + private viewPluginGenerator: ViewPluginGeneratorType, + private rendererRegistry: RendererRegistry, + private cellParser: ModernCellParser, + private settings: Settings, + private sync: Sync, + private vaultDb: Pick, + ) { + super(leaf); + } + + getViewType(): string { + return SQLSEAL_FILE_VIEW; + } + + getDisplayText(): string { + return this.file?.basename || 'SQLSeal'; + } + + async onOpen() { + this.contentEl.addClass("sqlseal-file-view-container"); + } + + async setViewData(data: string, clear: boolean): Promise { + this.fileContent = data; + await this.initializeView(); + } + + getViewData(): string { + if (this.editor) { + return this.editor.getCurrentQuery(); + } + return this.fileContent; + } + + clear(): void { + this.fileContent = ""; + this.contentEl.empty(); + } + + private async initializeView() { + if (!this.file) return; + + const fileExtension = this.file.extension.toLowerCase(); + + if (fileExtension === 'sqlite' || fileExtension === 'db') { + // Handle database files + await this.initializeDatabaseView(); + } else if (fileExtension === 'sql' || fileExtension === 'sqlseal') { + // Handle SQL text files + await this.initializeSQLView(); + } + } + + private async initializeDatabaseView() { + if (!this.file) return; + + try { + this.fileDb = await this.manager.getDatabaseConnection(this.file); + await this.fileDb.connect(); + this.schema = this.fileDb.getSchema(); + } catch (error) { + console.error("Failed to connect to database:", error); + return; + } + + await this.render(DEFAULT_SQLITE_QUERY); + } + + private async initializeSQLView() { + // For SQL files, use the vault database that was injected + // No need to connect as it's already available + + // Use file content as initial query, or default if empty + const initialQuery = this.fileContent.trim() || DEFAULT_SQL_QUERY; + await this.render(initialQuery); + } + + private async render(initialQuery: string) { + const codeblockProcessorGenerator = async (el: HTMLElement, source: string) => { + const ctx: MarkdownPostProcessorContext = { + docId: "", + sourcePath: this.file?.path || "", + frontmatter: {}, + } as any; + + // Create a database adapter to handle both MemoryDatabase and SqlSealDatabase + const dbAdapter = this.fileDb ? { + select: async (statement: string, frontmatter: Record) => { + const result = this.fileDb!.select(statement); + return { + data: result.data, + columns: Array.isArray(result.columns) ? result.columns : Object.keys(result.columns) + }; + }, + explain: async () => "" + } : this.vaultDb; + + const processor = new CodeblockProcessor( + el, + source, + ctx, + this.rendererRegistry, + dbAdapter, + this.cellParser, + this.settings, + this.app, + this.sync, + ); + await processor.onload(); + + // Resizing and layout configuration for explorer + const renderer = processor.renderer; + if ('communicator' in renderer && 'gridApi' in (renderer as any)['communicator']) { + const api: GridApi = (renderer.communicator as any).gridApi; + api.setGridOption('paginationAutoPageSize', true); + api.setGridOption('domLayout', 'normal'); // Override autoHeight for proper pagination + } + + await processor.render(); + return processor; + }; + + this.editor = new Editor( + codeblockProcessorGenerator, + this.viewPluginGenerator, + this.app, + initialQuery, + this.fileDb // Pass the file database (only for sqlite files) + ); + + // Override the editor's play function to include save functionality + const originalPlay = this.editor.play.bind(this.editor); + this.editor.play = async () => { + // Save the file first if it's a SQL file + if (this.file && (this.file.extension === 'sql' || this.file.extension === 'sqlseal')) { + const currentContent = this.editor?.getCurrentQuery() || ""; + if (currentContent !== this.fileContent) { + this.fileContent = currentContent; + await this.save(); + } + } + // Then run the query + await originalPlay(); + }; + + this.contentEl.empty(); + this.editor.render(this.contentEl); + } + + onPaneMenu(menu: Menu, source: "more-options" | "tab-header" | string): void { + if (this.file && (this.file.extension === 'sql' || this.file.extension === 'sqlseal')) { + menu.addItem(item => { + item.setTitle("Save and Run Query") + .setIcon("play") + .onClick(() => { + if (this.editor) { + this.editor.play(); + } + }); + }); + } + + menu.addItem(item => { + item.setTitle("Run Query") + .setIcon("play") + .onClick(() => { + if (this.editor) { + this.editor.play(); + } + }); + }); + + super.onPaneMenu(menu, source); + } + + getIcon(): IconName { + if (!this.file) return 'database'; + + const ext = this.file.extension.toLowerCase(); + if (ext === 'sql' || ext === 'sqlseal') { + return 'code'; + } + return 'database'; + } + + async onClose() { + // MemoryDatabase doesn't need explicit disconnection + } +} \ No newline at end of file diff --git a/src/modules/globalTables/FileConfig.ts b/src/modules/globalTables/FileConfig.ts index 4446659..0550ab6 100644 --- a/src/modules/globalTables/FileConfig.ts +++ b/src/modules/globalTables/FileConfig.ts @@ -10,6 +10,8 @@ export class FileConfig { private fileHandler: TFile | null = null async load() { + await this.handleMigration(); + this.fileHandler = this.vault.getFileByPath(this.path) if (!this.fileHandler) { return @@ -20,6 +22,30 @@ export class FileConfig { this.isLoaded = true } + private async handleMigration() { + // Check if current file exists + this.fileHandler = this.vault.getFileByPath(this.path) + if (this.fileHandler) { + return; // Current file exists, no migration needed + } + + // Check if this is a .sqlsealconfig file and look for old .sqlseal version + if (this.path.endsWith('.sqlsealconfig')) { + const oldPath = this.path.replace('.sqlsealconfig', '.sqlseal'); + const oldFile = this.vault.getFileByPath(oldPath); + + if (oldFile) { + // Migrate old file to new extension + try { + await this.vault.rename(oldFile, this.path); + console.log(`SQLSeal: Migrated config file from ${oldPath} to ${this.path}`); + } catch (error) { + console.error(`SQLSeal: Failed to migrate config file from ${oldPath} to ${this.path}:`, error); + } + } + } + } + async insert(v: T) { if (!this.isLoaded) { await this.load() diff --git a/src/modules/globalTables/GlobalTablesView.ts b/src/modules/globalTables/GlobalTablesView.ts index 81d3a3c..85ee5ad 100644 --- a/src/modules/globalTables/GlobalTablesView.ts +++ b/src/modules/globalTables/GlobalTablesView.ts @@ -23,7 +23,7 @@ export class GlobalTablesView extends ItemView { public sync: Sync, ) { super(leaf); - this.config = new FileConfig("__globalviews.sqlseal", vault); + this.config = new FileConfig("__globalviews.sqlsealconfig", vault); } getViewType() { From a82863bd8ae12b7d21539271247f62cdd4578cda Mon Sep 17 00:00:00 2001 From: Kacper Kula Date: Fri, 15 Aug 2025 14:18:44 +0100 Subject: [PATCH 2/3] chore: adding changeset --- .changeset/chilly-dodos-attend.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/chilly-dodos-attend.md diff --git a/.changeset/chilly-dodos-attend.md b/.changeset/chilly-dodos-attend.md new file mode 100644 index 0000000..cc9ffe3 --- /dev/null +++ b/.changeset/chilly-dodos-attend.md @@ -0,0 +1,5 @@ +--- +"sqlseal": minor +--- + +adding support for .sql and .sqlseal files From 38717981b0d897d399b5297e9358c311ef120999 Mon Sep 17 00:00:00 2001 From: Kacper Kula Date: Fri, 15 Aug 2025 14:31:38 +0100 Subject: [PATCH 3/3] feat: adding ability turn off .sql / .sqlite / .sqlseal / .db file view --- .changeset/chilly-dodos-attend.md | 2 +- src/modules/explorer/InitFactory.ts | 3 +- src/modules/settings/SQLSealSettingsTab.ts | 2 + src/modules/settings/init.ts | 4 +- .../settingsTabSection/SettingsSQLControls.ts | 70 +++++++++++++++++++ 5 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 src/modules/settings/settingsTabSection/SettingsSQLControls.ts diff --git a/.changeset/chilly-dodos-attend.md b/.changeset/chilly-dodos-attend.md index cc9ffe3..db964a4 100644 --- a/.changeset/chilly-dodos-attend.md +++ b/.changeset/chilly-dodos-attend.md @@ -1,5 +1,5 @@ --- -"sqlseal": minor +"sqlseal": major --- adding support for .sql and .sqlseal files diff --git a/src/modules/explorer/InitFactory.ts b/src/modules/explorer/InitFactory.ts index 6111eec..bf5297e 100644 --- a/src/modules/explorer/InitFactory.ts +++ b/src/modules/explorer/InitFactory.ts @@ -64,8 +64,7 @@ export class InitFactory { return new SQLSealFileView(leaf, dbManager, viewPluginGenerator, rendererRegistry, cellParser, settings, sync, db) }) - // Register extensions for SQLSeal file view - plugin.registerExtensions(['sql', 'sqlseal', 'sqlite', 'db'], SQLSEAL_FILE_VIEW) + // Extensions for SQLSeal file view are registered by SettingsSQLControls plugin.addCommand({ id: 'sqlseal-command-explorer', diff --git a/src/modules/settings/SQLSealSettingsTab.ts b/src/modules/settings/SQLSealSettingsTab.ts index 432f903..daec150 100644 --- a/src/modules/settings/SQLSealSettingsTab.ts +++ b/src/modules/settings/SQLSealSettingsTab.ts @@ -8,6 +8,7 @@ export interface SQLSealSettings { enableViewer: boolean; enableEditing: boolean; enableJSONViewer: boolean; + enableSQLViewer: boolean; enableDynamicUpdates: boolean; enableSyntaxHighlighting: boolean; defaultView: 'grid' | 'markdown' | 'html'; @@ -18,6 +19,7 @@ export const DEFAULT_SETTINGS: SQLSealSettings = { enableViewer: true, enableEditing: true, enableJSONViewer: true, + enableSQLViewer: true, enableDynamicUpdates: true, enableSyntaxHighlighting: true, defaultView: 'grid', diff --git a/src/modules/settings/init.ts b/src/modules/settings/init.ts index 9f034e7..fb02f9f 100644 --- a/src/modules/settings/init.ts +++ b/src/modules/settings/init.ts @@ -5,6 +5,7 @@ import { SQLSealSettingsTab } from "./SQLSealSettingsTab"; import { Settings } from "./Settings"; import { SettingsCSVControls } from "./settingsTabSection/SettingsCSVControls"; import { SettingsJsonControls } from "./settingsTabSection/SettingsJsonControls"; +import { SettingsSQLControls } from "./settingsTabSection/SettingsSQLControls"; import { ViewPluginGeneratorType } from "../syntaxHighlight/viewPluginGenerator"; @(makeInjector()(["plugin", "settingsTab", "app", "settings", "viewPluginGenerator"])) @@ -18,8 +19,9 @@ export class SettingsInit { ) { const csvControl = new SettingsCSVControls(settings, app, plugin, viewPluginGenerator); const jsonControl = new SettingsJsonControls(settings, app, plugin, viewPluginGenerator); + const sqlControl = new SettingsSQLControls(settings, app, plugin); - const controls = [csvControl, jsonControl]; + const controls = [csvControl, jsonControl, sqlControl]; settingsTab.registerControls(...controls); diff --git a/src/modules/settings/settingsTabSection/SettingsSQLControls.ts b/src/modules/settings/settingsTabSection/SettingsSQLControls.ts new file mode 100644 index 0000000..bb1043e --- /dev/null +++ b/src/modules/settings/settingsTabSection/SettingsSQLControls.ts @@ -0,0 +1,70 @@ +import { App, Plugin, Setting } from "obsidian"; +import { Settings } from "../Settings"; +import { + checkTypeViewAvaiability, +} from "../utils/viewInspector"; +import { SettingsControls } from "./SettingsControls"; +import { SQLSEAL_FILE_VIEW } from "../../explorer/SQLSealFileView"; + +export class SettingsSQLControls extends SettingsControls { + private registeredView: string | null = null; + + constructor(settings: Settings, app: App, plugin: Plugin) { + super(settings, app, plugin) + } + + register() { + if (this.settings.get("enableSQLViewer")) { + const view = checkTypeViewAvaiability(this.app, 'sql'); + if (view && view !== SQLSEAL_FILE_VIEW) { + this.registeredView = view; + return; + } + + // Register extensions when enabling + this.plugin.registerExtensions(['sql', 'sqlseal', 'sqlite', 'db'], SQLSEAL_FILE_VIEW); + } + } + + unregister() { + this.app.workspace.detachLeavesOfType(SQLSEAL_FILE_VIEW); + (this.app as any).viewRegistry.unregisterExtensions([ + 'sql', 'sqlseal', 'sqlite', 'db' + ]); + } + + display(el: HTMLDivElement) { + el.empty(); + el.createEl("h2", { text: "SQL Explorer" }); + + const view = checkTypeViewAvaiability(this.app, "sql"); + + if (view && view !== SQLSEAL_FILE_VIEW) { + el.createDiv({ + text: "SQL files are already handled by different plugin. To enable SQLSeal SQL viewer, disable other plugin that handles it", + cls: "sqlseal-settings-warn", + }); + return; + } + + new Setting(el) + .setName("Enable SQL Explorer") + .setDesc( + "Enables SQL, SQLSeal, SQLite and DB files in your vault to preview then and execture queries.", + ) + .addToggle((toggle) => + toggle + .setValue(this.settings.get("enableSQLViewer")) + .onChange(async (value) => { + this.settings.set("enableSQLViewer", !!value); + if (!!value) { + // Enabled + this.register(); + } else { + // Disabled + this.unregister(); + } + }), + ); + } +} \ No newline at end of file