feat: added support for .sql and .sqlseal files (#184)

* feat: added support for .sql and .sqlseal files

* chore: adding changeset

* feat: adding ability turn off .sql / .sqlite / .sqlseal / .db file view
This commit is contained in:
Kacper Kula 2025-08-15 15:39:06 +02:00 committed by GitHub
parent 10eb87803c
commit c4278c2a9d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 346 additions and 113 deletions

View file

@ -0,0 +1,5 @@
---
"sqlseal": major
---
adding support for .sql and .sqlseal files

View file

@ -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
}
});
}
}
}

View file

@ -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<void> {
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'
}
}

View file

@ -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,12 @@ 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)
// Extensions for SQLSeal file view are registered by SettingsSQLControls
plugin.addCommand({
id: 'sqlseal-command-explorer',

View file

@ -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<SqlSealDatabase, 'select' | 'explain'>,
) {
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<void> {
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<string, unknown>) => {
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
}
}

View file

@ -10,6 +10,8 @@ export class FileConfig<T> {
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<T> {
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()

View file

@ -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() {

View file

@ -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',

View file

@ -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<SettingsModule>()(["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);

View file

@ -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();
}
}),
);
}
}