mirror of
https://github.com/h-sphere/sql-seal.git
synced 2026-07-22 10:10:28 +00:00
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:
parent
10eb87803c
commit
c4278c2a9d
10 changed files with 346 additions and 113 deletions
5
.changeset/chilly-dodos-attend.md
Normal file
5
.changeset/chilly-dodos-attend.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"sqlseal": major
|
||||
---
|
||||
|
||||
adding support for .sql and .sqlseal files
|
||||
|
|
@ -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
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
217
src/modules/explorer/SQLSealFileView.ts
Normal file
217
src/modules/explorer/SQLSealFileView.ts
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue