Merge pull request #71 from h-sphere/feat/adding-flags

feat: adding flags and ability to run explain
This commit is contained in:
Kacper Kula 2025-02-04 14:35:31 +00:00 committed by GitHub
commit 2c7a2ac99c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 224 additions and 30 deletions

View file

@ -1,3 +1,7 @@
# 0.22.3 (2025-02-04)
- Ability to enable / disable query refreshing individually for each query (REFRESH / NO REFRESH statement)
- (Advanced) Added ability to show SQL execution plan for the query
# 0.22.2 (2025-02-01)
- Added index to tags.tag which was missed in the last release

View file

@ -34,6 +34,7 @@ export default defineConfig({
{ text: 'Inline codeblocks', link: '/inline-codeblocks' },
{ text: 'Links and Images', link: '/links-and-images' },
{ text: 'CSV Viewer', link: '/csv-viewer' },
{ text: 'Query Configuration', link: '/query-configuration' },
{ text: 'Troubleshooting', link: '/troubleshooting' },
{ text: 'Future Plans', link: '/future-plans' },
{ text: 'Changelog', link: '/changelog' }

BIN
docs/explain.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View file

@ -0,0 +1,28 @@
# Query Configuration
You can specify the following configuration to change behaviour of your query.
Configuration statements go always after all `TABLE` statements and before view / renderer definition or SELECT definition.
Example:
```sqlseal
TABLE data = file(data.csv)
REFRESH
EXPLAIN
GRID
SELECT * FROM data
```
## Refresh
You can use `REFRESH / NO REFRESH` to change default refreshing behaviour.
When you set `NO REFRESH`, the query will execute once and will not listen to any changes to the underlying data. This is especially helpful if you run very expensive query and want to have full control on when it refreshes.
`REFRESH` will refresh the query every time the underlying data changes. It listens only to the tables you actually use in the query.
## Explain
You can use `EXPLAIN` statment to show execution plan of the query. This is helpful when you want to analyse the queries performed to optimise them or see where are bottlenecks.
For majority of the queries explain might be very simple but your more advanced it can bring some insights:
![Explain example](./explain.png)

View file

@ -1,7 +1,7 @@
{
"id": "sqlseal",
"name": "SQLSeal",
"version": "0.22.2",
"version": "0.22.3",
"minAppVersion": "0.15.0",
"description": "Use SQL in your notes to query your vault files and CSV content.",
"author": "hypersphere",

View file

@ -1,6 +1,6 @@
{
"name": "sqlseal",
"version": "0.22.2",
"version": "0.22.3",
"description": "A plugin for Obsidian that allows you to run SQL queries on your notes.",
"main": "main.js",
"scripts": {

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

View file

@ -38,5 +38,6 @@
"0.21.3": "0.15.0",
"0.22.0": "0.15.0",
"0.22.1": "0.15.0",
"0.22.2": "0.15.0"
"0.22.2": "0.15.0",
"0.22.3": "0.15.0"
}