From 3363316928046ab9a26c6300aa0f1212a325802a Mon Sep 17 00:00:00 2001 From: Kacper Kula Date: Tue, 4 Feb 2025 10:43:47 +0000 Subject: [PATCH] feat: adding flags and ability to run explain --- src/codeblockHandler/CodeblockProcessor.ts | 32 +++++++- src/database/database.ts | 18 +++++ src/grammar/newParser.test.ts | 1 - src/grammar/parseIntermediateContent.test.ts | 78 ++++++++++++++++++++ src/grammar/parseIntermediateContent.ts | 58 +++++++++++++++ src/renderer/rendererRegistry.ts | 27 +------ 6 files changed, 187 insertions(+), 27 deletions(-) create mode 100644 src/grammar/parseIntermediateContent.test.ts create mode 100644 src/grammar/parseIntermediateContent.ts diff --git a/src/codeblockHandler/CodeblockProcessor.ts b/src/codeblockHandler/CodeblockProcessor.ts index e50180f..c226de9 100644 --- a/src/codeblockHandler/CodeblockProcessor.ts +++ b/src/codeblockHandler/CodeblockProcessor.ts @@ -9,11 +9,15 @@ import { transformQuery } from "../sql/sqlTransformer"; import { displayError, displayNotice } from "../utils/ui"; import SqlSealPlugin from "../main"; import { registerObservers } from "../utils/registerObservers"; +import { IntermediateContent, parseIntermediateContent } from "src/grammar/parseIntermediateContent"; export class CodeblockProcessor extends MarkdownRenderChild { registrator: OmnibusRegistrator renderer: RenderReturn + private flags: IntermediateContent['flags'] + private extrasEl: HTMLElement + private explainEl: HTMLElement constructor( private el: HTMLElement, @@ -25,6 +29,7 @@ export class CodeblockProcessor extends MarkdownRenderChild { private app: App, private sync: Sync) { super(el) + this.registrator = this.sync.getRegistrator() } @@ -41,7 +46,23 @@ export class CodeblockProcessor extends MarkdownRenderChild { } } - this.renderer = this.rendererRegistry.prepareRender(results.intermediateContent)(this.el) + const { intermediateContent } = results + const config = parseIntermediateContent(intermediateContent, { + flags: { + refresh: this.plugin.settings.enableDynamicUpdates, + explain: false + } + }) + + this.flags = config.flags + + this.extrasEl = this.el.createDiv({ cls: 'sqlseal-extras-container' }) + if (config.flags.explain) { + this.explainEl = this.extrasEl.createEl('pre', { cls: 'sqlseal-extras-explain-container' }) + } + const rendererEl = this.el.createDiv({ cls: 'sqlseal-renderer-container' }) + + this.renderer = this.rendererRegistry.prepareRender(config.renderer.toLowerCase(), config.rendererArguments)(rendererEl) // FIXME: probably should save the one before transform and perform transform every time we execute it. this.query = results.queryPart @@ -63,7 +84,7 @@ export class CodeblockProcessor extends MarkdownRenderChild { const res = transformQuery(this.query, registeredTablesForContext) const transformedQuery = res.sql - if (this.plugin.settings.enableDynamicUpdates) { + if (this.flags.refresh) { registerObservers({ bus: this.registrator, callback: () => this.render(), @@ -78,6 +99,13 @@ export class CodeblockProcessor extends MarkdownRenderChild { return } const fileCache = this.app.metadataCache.getFileCache(file) + + if (this.flags.explain) { + // Rendering explain + const result = await this.db.explain(transformedQuery, fileCache?.frontmatter ?? { }) + this.explainEl.textContent = result + } + const { data, columns } = await this.db.select(transformedQuery, fileCache?.frontmatter ?? {}) this.renderer.render({ data, columns }) } catch (e) { diff --git a/src/database/database.ts b/src/database/database.ts index c80220c..da9cac3 100644 --- a/src/database/database.ts +++ b/src/database/database.ts @@ -96,4 +96,22 @@ export class SqlSealDatabase { const result = await this.db.select(statement, frontmatter) return result } + + async explain(statement: string, frontmatter: Record) { + const explainResults = await this.db.explainQuery(statement, frontmatter) + let strResult = ''; + const map = new Map() + const INDENT_INCREASE = 4 + map.set(0, -INDENT_INCREASE) + for (const result of explainResults) { + const parent = parseInt((result.parent as unknown as string) ?? 0, 10) + const indent = map.get(parent)! + INDENT_INCREASE + for (let i=0;i { it('should properly parse select only', () => { diff --git a/src/grammar/parseIntermediateContent.test.ts b/src/grammar/parseIntermediateContent.test.ts new file mode 100644 index 0000000..0f4959c --- /dev/null +++ b/src/grammar/parseIntermediateContent.test.ts @@ -0,0 +1,78 @@ +import { parseIntermediateContent } from "./parseIntermediateContent" + +describe('Parse Intermediate Content', () => { + it('should properly parse content without any flags', () => { + expect(parseIntermediateContent(` + + HTML`, {})).toEqual({ + renderer: 'HTML', + rendererArguments: '', + flags: { + explain: false, + refresh: true + } + }) + }) + + it('should properly set no refresh', () => { + expect(parseIntermediateContent(` + NO REFRESH + HTML`, {})).toEqual({ + renderer: 'HTML', + rendererArguments: '', + flags: { + explain: false, + refresh: false + } + }) + }) + + it('should properly set explain', () => { + expect(parseIntermediateContent(` + EXPLAIN + HTML`, {})).toEqual({ + renderer: 'HTML', + rendererArguments: '', + flags: { + explain: true, + refresh: true + } + }) + }) + + it('should properly set both flags', () => { + expect(parseIntermediateContent(` + EXPLAIN + NO REFRESH + HTML`, {})).toEqual({ + renderer: 'HTML', + rendererArguments: '', + flags: { + explain: true, + refresh: false + } + }) + }) + + it('should use initial values when calculating', () => { + expect(parseIntermediateContent(`HTML`, { + flags: { explain: true, refresh: false } + })).toEqual({ + flags: { explain: true, refresh: false }, + renderer: 'HTML', + rendererArguments: '' + }) + }) + + it('should allow for flag modifiers to be used in lower case', () => { + expect(parseIntermediateContent(` + explain + no refresh + grid + `, {})).toEqual({ + flags: { explain: true, refresh: false }, + renderer: 'GRID', + rendererArguments: '' + }) + }) +}) \ No newline at end of file diff --git a/src/grammar/parseIntermediateContent.ts b/src/grammar/parseIntermediateContent.ts new file mode 100644 index 0000000..7701d61 --- /dev/null +++ b/src/grammar/parseIntermediateContent.ts @@ -0,0 +1,58 @@ +import { assign } from "lodash" + +export interface IntermediateContent { + flags: { + refresh: boolean, + explain: boolean + }, + renderer: string + rendererArguments: string +} + +const defaultContent = () => ({ + flags: { + refresh: true, + explain: false + }, + renderer: 'GRID', + rendererArguments: '' +} satisfies IntermediateContent) + +const FLAGS = { + 'REFRESH': (content: IntermediateContent) => { + content.flags.refresh = true + }, + 'NO REFRESH': (content: IntermediateContent) => { + content.flags.refresh = false + }, + 'EXPLAIN': (content: IntermediateContent) => { + content.flags.explain = true + } +} as Record + +export const parseIntermediateContent = (content: string, initialConfig: Partial) => { + const config = assign(defaultContent(), initialConfig) + let processingFlags = true + const rendererArguments = [] + for (const row of content.split('\n')) { + const trimmedRow = row.trim().toUpperCase() + if (!trimmedRow) { + continue // empty row + } + if (processingFlags) { + if (!FLAGS[trimmedRow]) { + processingFlags = false + const [renderer, ...rest ] = trimmedRow.split(' ') + config.renderer = renderer.toUpperCase() + rendererArguments.push(rest.join(' ')) + } else { + const fn = FLAGS[trimmedRow] + fn(config) + } + } else { + rendererArguments.push(trimmedRow) + } + } + config.rendererArguments = rendererArguments.join('\n') + return config +} \ No newline at end of file diff --git a/src/renderer/rendererRegistry.ts b/src/renderer/rendererRegistry.ts index 9cc284a..41f6fb6 100644 --- a/src/renderer/rendererRegistry.ts +++ b/src/renderer/rendererRegistry.ts @@ -43,35 +43,14 @@ export class RendererRegistry { this.renderers.delete(uniqueName) } - splitConfig(config: string) { - if (config.length === 0) { - return { - type: this.default, - config: '' - } - } - const firstSpace = config.indexOf(" ") - if (firstSpace < 0) { - return { - type: config.toLowerCase(), - config: '' - } - } - return { - type: config.substring(0, firstSpace).toLowerCase(), - config: config.substring(firstSpace) - } - } - - prepareRender(inputConfig: string) { - const { type, config } = this.splitConfig(inputConfig) + prepareRender(type: string, config: string) { if (!this.renderersByKey.has(type)) { throw new Error(`Renderer does not exist for ${type}`) } - const rendererConfig = this.renderersByKey.get(type)! + const rendererConfig = this.renderersByKey.get(type.toLowerCase())! const elConfig = rendererConfig.validateConfig(config) return (el: HTMLElement) => { return rendererConfig.render(elConfig, el) } } -} \ No newline at end of file +}