Using libsyntaqlite from JavaScript

This guide picks up where the libsyntaqlite for JavaScript tutorial leaves off. It covers formatter options, building a schema from objects, rendered reports, parse errors, tokens, the built-in language server, and loading the package in the browser. Each example starts by loading the engine as the tutorial does.

Customize formatting

format takes options for the line width, indentation, keyword case, and trailing semicolons:

import {DialectManager, Engine} from "syntaqlite";

const engine = new Engine();
await engine.load();
await new DialectManager().loadDefault(engine);

const sql = "SELECT id, name, email FROM users WHERE active = 1 AND role = 'admin'";
process.stdout.write(engine.format(sql, {lineWidth: 40, indentWidth: 4, keywordCase: "lower"}));

try {
  engine.format("SELECT FROM users");
} catch (err) {
  console.log(`error: ${err.message}`);
}
select id, name, email
from users
where
    active = 1
    and role = 'admin';
error: syntax error near 'FROM'

SQL that does not parse throws. See Formatting options for what each setting does.

Build a schema from objects

The tutorial loads the schema from CREATE TABLE statements. If your program already knows its tables, pass them as tables and views instead. A column is a name, or {name, kind} where kind is "normal", "generated", or "hidden". A table without columns accepts any column name:

import {DialectManager, Engine} from "syntaqlite";

const engine = new Engine();
await engine.load();
await new DialectManager().loadDefault(engine);

const result = engine.analyze(
  "SELECT name_lower FROM users; SELECT anything FROM events; SELECT nmae FROM user_names",
  {
    tables: [
      {name: "users", columns: ["id", "name", {name: "name_lower", kind: "generated"}]},
      // Columns unknown: any column of this table is accepted.
      {name: "events"},
    ],
    views: [{name: "user_names", columns: ["name"]}],
  },
);
for (const d of result.diagnostics) {
  console.log(`${d.severity} at bytes ${d.start_offset}-${d.end_offset}: ${d.message}`);
}
warning at bytes 66-70: unknown column 'nmae'

schemaDdl, tables, and views can be combined in one call. Pass strictSchema: true to report unknown names as errors.

Show a report

analyzeText takes the same options as analyze and returns the diagnostics as a terminal-style report, with the source shown under each one:

import {DialectManager, Engine} from "syntaqlite";

const engine = new Engine();
await engine.load();
await new DialectManager().loadDefault(engine);

const report = engine.analyzeText("SELECT id, nme FROM users WHERE active = 1", {
  schemaDdl: "CREATE TABLE users (id INTEGER, name TEXT, email TEXT, active INTEGER);",
  strictSchema: true,
  sourceName: "query.sql",
});
process.stdout.write(report);
error: unknown column 'nme'
 --> query.sql:1:12
  |
1 | SELECT id, nme FROM users WHERE active = 1
  |            ^~~
  = help: did you mean 'name'?

Parse errors

parse never throws for bad SQL. It returns the statements that parsed and, separately, the errors for those that didn't:

import {DialectManager, Engine} from "syntaqlite";

const engine = new Engine();
await engine.load();
await new DialectManager().loadDefault(engine);

const {statements, errors} = engine.parse("SELECT FROM; SELECT 1");
console.log(statements.map((s) => s.type));
for (const e of errors) {
  console.log(`${e.severity} at bytes ${e.start_offset}-${e.end_offset}: ${e.message}`);
}
[ 'SelectStmt' ]
error at bytes 7-11: syntax error near 'FROM'

Tokens

tokenize returns every token, including whitespace, with its byte offset and a category:

import {DialectManager, Engine} from "syntaqlite";

const engine = new Engine();
await engine.load();
await new DialectManager().loadDefault(engine);

for (const tok of engine.tokenize("SELECT max(x) FROM t")) {
  console.log(`${String(tok.offset).padStart(2)} ${JSON.stringify(tok.text).padEnd(8)} ${tok.category}`);
}
 0 "SELECT" keyword
 6 " "      other
 7 "max"    identifier
10 "("      punctuation
11 "x"      identifier
12 ")"      punctuation
13 " "      other
14 "FROM"   keyword
18 " "      other
19 "t"      identifier

Language server

The package includes a language server, the same one syntaqlite lsp runs, inside the engine. Send it standard LSP messages with lspMessage, which returns the messages the server sends back:

import {DialectManager, Engine} from "syntaqlite";

const engine = new Engine();
await engine.load();
await new DialectManager().loadDefault(engine);
engine.setSessionContextDdl("CREATE TABLE users (id INTEGER, name TEXT);");

engine.lspMessage({jsonrpc: "2.0", id: 1, method: "initialize", params: {capabilities: {}}});
engine.lspMessage({jsonrpc: "2.0", method: "initialized", params: {}});

const out = engine.lspMessage({
  jsonrpc: "2.0",
  method: "textDocument/didOpen",
  params: {
    textDocument: {uri: "file:///query.sql", languageId: "sql", version: 1, text: "SELECT nme FROM users"},
  },
});
const published = out.find((m) => m.method === "textDocument/publishDiagnostics");
for (const d of published.params.diagnostics) {
  const {start, end} = d.range;
  console.log(`${start.line}:${start.character}-${end.character} ${d.message}`);
}
0:7-10 unknown column 'nme' (did you mean 'name'?)

setSessionContextDdl gives the server the schema to check against. In an editor, run the engine in a Web Worker and connect it to a standard LSP client such as the ones for CodeMirror or Monaco. attachLspPort wires the server to the worker's message port:

// worker.js
import {attachLspPort, DialectManager, Engine} from "syntaqlite";

const engine = new Engine();
await engine.load();
await new DialectManager().loadDefault(engine);
attachLspPort(engine, self);

Use it in the browser

The package loads its WebAssembly files from its wasm/ directory, which it finds relative to its own JavaScript files. If your bundler copies the package as is, there is nothing to configure. Otherwise, copy the whole wasm/ directory to where you serve static files and pass its URL as wasmBaseUrl. The runtime and the SQLite dialect are both loaded from there:

import {DialectManager, Engine} from "syntaqlite";

const engine = new Engine({wasmBaseUrl: "/assets/syntaqlite/"});
await engine.load();
await new DialectManager().loadDefault(engine);

loadDefault throws if the dialect cannot be loaded, for example because a file is missing.

Next steps