mirror of
https://github.com/h-sphere/sql-seal.git
synced 2026-07-22 10:10:28 +00:00
Merge pull request #71 from h-sphere/feat/adding-flags
feat: adding flags and ability to run explain
This commit is contained in:
commit
2c7a2ac99c
13 changed files with 224 additions and 30 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
BIN
docs/explain.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
28
docs/query-configuration.md
Normal file
28
docs/query-configuration.md
Normal 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:
|
||||
|
||||

|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import { parseLanguage } from './newParser'
|
||||
jest.mock('../datamodel/syncStrategy/MarkdownTableSyncStrategy')
|
||||
|
||||
describe('Parser', () => {
|
||||
it('should properly parse select only', () => {
|
||||
|
|
|
|||
78
src/grammar/parseIntermediateContent.test.ts
Normal file
78
src/grammar/parseIntermediateContent.test.ts
Normal 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: ''
|
||||
})
|
||||
})
|
||||
})
|
||||
58
src/grammar/parseIntermediateContent.ts
Normal file
58
src/grammar/parseIntermediateContent.ts
Normal 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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
Loading…
Reference in a new issue