From d761e33c38055b4de399c452aa90ff2b5ee0220d Mon Sep 17 00:00:00 2001 From: Kacper Kula Date: Thu, 13 Feb 2025 11:32:43 +0000 Subject: [PATCH] feat: poc of the grammar --- package.json | 1 + pnpm-lock.yaml | 9 ++++ src/grammar/ohmGrammar.ts | 22 ++++++++++ src/grammar/parser.test.ts | 65 ++++++++++++++++++++++++++++ src/grammar/parser.ts | 89 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 186 insertions(+) create mode 100644 src/grammar/ohmGrammar.ts create mode 100644 src/grammar/parser.test.ts create mode 100644 src/grammar/parser.ts diff --git a/package.json b/package.json index a3e6c73..8aabaa2 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "jsonpath": "^1.1.1", "lodash": "^4.17.21", "markdown-table-ts": "^1.0.3", + "ohm-js": "^17.1.0", "papaparse": "^5.4.1", "sql-parser-cst": "^0.32.0", "unidecode": "^1.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8aac7b1..d1b726f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,6 +56,9 @@ importers: markdown-table-ts: specifier: ^1.0.3 version: 1.0.3 + ohm-js: + specifier: ^17.1.0 + version: 17.1.0 papaparse: specifier: ^5.4.1 version: 5.4.1 @@ -2616,6 +2619,10 @@ packages: '@codemirror/state': ^6.0.0 '@codemirror/view': ^6.0.0 + ohm-js@17.1.0: + resolution: {integrity: sha512-xc3B5dgAjTBQGHaH7B58M2Pmv6WvzrJ/3/7LeUzXNg0/sY3jQPdSd/S2SstppaleO77rifR1tyhdfFGNIwxf2Q==} + engines: {node: '>=0.12.1'} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -6156,6 +6163,8 @@ snapshots: '@types/codemirror': 5.60.8 moment: 2.29.4 + ohm-js@17.1.0: {} + once@1.4.0: dependencies: wrappy: 1.0.2 diff --git a/src/grammar/ohmGrammar.ts b/src/grammar/ohmGrammar.ts new file mode 100644 index 0000000..6790cdd --- /dev/null +++ b/src/grammar/ohmGrammar.ts @@ -0,0 +1,22 @@ +/*** + SQLSealLang { + Grammar = (GrammarEntry | Flag)* SelectStmt* + SelectStmt = ("WITH" | "SELECT") any+ + GrammarEntry = TableExpression | RendererExpression + Flag = "REPEAT" | "NO REPEAT" | "EXPLAIN" + TableExpression = "TABLE" identifier "=" "file(" filename ")" + identifier = alnum+ + filename = (alnum | ".")+ + RendererExpression = "GRID" anyObject -- grid + | "HTML" -- html + | "MARKDOWN" --markdown + NewLine = "\n" + sp = " " + blank = sp* nl // blank line has only newline + endline = (~nl any)+ end + objectContent = (alnum | "." | ":" | "," | sp | nl | tab)+ + anyObject = "{" (~"}" any)* "}" + nl = "\n" // new line + tab = "\t" +} + */ \ No newline at end of file diff --git a/src/grammar/parser.test.ts b/src/grammar/parser.test.ts new file mode 100644 index 0000000..d02a893 --- /dev/null +++ b/src/grammar/parser.test.ts @@ -0,0 +1,65 @@ +import { parse } from './parser' + +describe('Ohm parser', () => { + + it('should parse simple table expression', () => { + expect(parse('TABLE x = file(a.csv)')).toEqual({ + query: '', + tables: [{ filename: 'a.csv', type: 'csv', identifier: 'x'}], + flags: { + explain: false, + refresh: true + }, + renderer: { + name: 'GRID', + options: '' + } + }) + }) + + it('should parse SELECT statement alone', () => { + expect(parse('SELECT * FROM files')).toEqual({ + query: 'SELECT * FROM files', + tables: [], + flags: { explain: false, refresh: true }, + renderer: { name: 'GRID', options: '' } + }) + }) + + it('should not parse incorrect statement', () => { + expect(() => parse('this is not valid entry')).toThrow() + }) + + it('should parse statement with table and select', () => { + expect(parse(`TABLE x = file(a.csv) +SELECT * FROM x`)).toEqual({ + tables: [{ type: 'csv', filename: 'a.csv', identifier: 'x' }], + flags: { explain: false, refresh: true }, + renderer: { name: 'GRID', options: '' }, + query: 'SELECT * FROM x' +}) + }) + + it('should have table and flags', () => { + expect(parse(` + TABLE x = file(a.csv) + REFRESH + EXPLAIN + SELECT * FROM x`)).toEqual({ + tables: [{ + filename: 'a.csv', + identifier: 'x', + type: 'csv' + }], + renderer: { + name: 'GRID', + options: '' + }, + flags: { + explain: false, + refresh: true + }, + query: 'SELECT * FROM x' + }) + }) +}) \ No newline at end of file diff --git a/src/grammar/parser.ts b/src/grammar/parser.ts new file mode 100644 index 0000000..96e64b4 --- /dev/null +++ b/src/grammar/parser.ts @@ -0,0 +1,89 @@ +import * as ohm from 'ohm-js'; + + +const SQLSealGrammar = String.raw` +SQLSealLang { + Grammar = (TableExpression | RendererExpression | FlagExpression)* SelectStmt* + SelectStmt = ("WITH" | "SELECT") any+ + FlagExpression = "REFRESH" -- refresh + | "NO REFRESH" -- norefresh + | "EXPLAIN" -- explain + TableExpression = "TABLE" identifier "=" "file(" filename ")" + identifier = alnum+ + filename = (alnum | ".")+ + RendererExpression = "GRID" anyObject -- grid + | "HTML" -- html + | "MARKDOWN" --markdown + anyObject = "{" (~"}" any)* "}" +} +` + +const generateSemantic = (grammar: ohm.Grammar) => { + const s = grammar.createSemantics() + s.addOperation('toObject', { + Grammar: (entries, selectStatement) => { + console.log('GRAMMAR', entries, selectStatement) + const res = { + flags: { + refresh: true, + explain: false + }, + renderer: { + name: 'GRID', + options: '' + }, + tables: [] as any[], + query: '' + } + if (entries.children.length) { + entries.children.forEach(c => { + console.log(c.ctorName) + switch (c.ctorName) { + case 'TableExpression': + res.tables.push(c.toObject()) + break; + case 'RendererExpression': + res.renderer = c.toObject() + break + case 'Flag': + res.flags = {...res.flags, ...c.toObject()} + break + } + }) + } + if (selectStatement) { + res.query = selectStatement.sourceString + } + + return res + }, + TableExpression: (_table, identifier, _eq, _file, filename, _close) => { + return { identifier: identifier.sourceString , filename: filename.sourceString, type: 'csv' } + }, + FlagExpression_refresh: (v) => { + return { refresh: true } + }, + FlagExpression_norefresh: (v) => { + return {refresh: false} + }, + FlagExpression_explain: (v) => { + return { explain: true } + } + }) + + return s +} + +export const parse = (query: string) => { + + + const grammar = ohm.grammar(SQLSealGrammar) + const match = grammar.match(query) + if (match.succeeded()) { + // Converting + const s = generateSemantic(grammar)(match) + return s.toObject() + } else { + throw new Error(match.message || 'Unknown parsing error') + } +} \ No newline at end of file