feat: adding flags and ability to run explain

This commit is contained in:
Kacper Kula 2025-02-04 10:43:47 +00:00
parent 790670205e
commit 3363316928
6 changed files with 187 additions and 27 deletions

View file

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

View file

@ -96,4 +96,22 @@ export class SqlSealDatabase {
const result = await this.db.select(statement, frontmatter)
return result
}
async explain(statement: string, frontmatter: Record<string, unknown>) {
const explainResults = await this.db.explainQuery(statement, frontmatter)
let strResult = '';
const map = new Map<number, number>()
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<indent;i++) {
strResult += ' '
}
strResult += result.detail + "\n"
map.set(result.id as number, indent)
}
return strResult
}
}

View file

@ -1,5 +1,4 @@
import { parseLanguage } from './newParser'
jest.mock('../datamodel/syncStrategy/MarkdownTableSyncStrategy')
describe('Parser', () => {
it('should properly parse select only', () => {

View file

@ -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: ''
})
})
})

View file

@ -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<string, any>
export const parseIntermediateContent = (content: string, initialConfig: Partial<IntermediateContent>) => {
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
}

View file

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