SQL Playground — JavaScript source
Run real SQL on sample datasets - or your own schema - right in the browser. Write queries, see formatted results instantly, and export or share them. Powered by sql.js (SQLite WASM); 100% client-side.
This is the JavaScript implementation — the same logic the interactive tool runs, in a shareable, citable form.
/**
* sql-playground - polyglot showcase port (JavaScript)
*
* Pure helper logic for the CosmoDev "SQL Playground" tool: splitting a SQL
* script into individual statements, classifying read-only statements, and
* rendering result sets (columns + rows) as CSV / Markdown / JSON. Also
* extracts a lightweight schema summary (table -> column list) from a CREATE
* TABLE script.
*
* Ported from src/lib/sql-playground.ts (the canonical TypeScript lib that
* powers the live tool). Functionally equivalent: same inputs -> same outputs.
*
* Display source - part of CosmoDev's polyglot tool pages.
*/
// ---------------------------------------------------------------------------
// Statement splitting
// ---------------------------------------------------------------------------
/**
* Split a SQL script into top-level statements on `;`, respecting single-quote
* string literals. SQL string-literal escaping is honoured: a doubled `''`
* inside a string is treated as an escaped quote (per the SQL standard), not as
* the end of the string. Whitespace-only statements are dropped.
*
* @param {string} sql
* @returns {string[]} trimmed, non-empty statements
*/
export function splitStatements(sql) {
const stmts = [];
let current = '';
let inString = false;
for (let i = 0; i < sql.length; i++) {
const ch = sql[i];
if (inString) {
current += ch;
if (ch === "'") {
// Doubled single quote = an escaped literal quote, consume both chars.
if (i + 1 < sql.length && sql[i + 1] === "'") {
current += sql[i + 1];
i++;
} else {
inString = false;
}
}
} else if (ch === "'") {
inString = true;
current += ch;
} else if (ch === ';') {
const trimmed = current.trim();
if (trimmed) stmts.push(trimmed);
current = '';
} else {
current += ch;
}
}
// Flush any trailing statement that wasn't terminated by `;`.
const trimmed = current.trim();
if (trimmed) stmts.push(trimmed);
return stmts;
}
// ---------------------------------------------------------------------------
// Read-only classification
// ---------------------------------------------------------------------------
/**
* True when the statement is a read-only one (safe to run against a snapshot
* without mutating state). Recognises the common query-leading keywords.
*
* @param {string} sql
* @returns {boolean}
*/
export function isReadOnlyStatement(sql) {
return /^(SELECT|WITH|VALUES|EXPLAIN|PRAGMA)\b/i.test(sql.trim());
}
// ---------------------------------------------------------------------------
// Scalar formatting
// ---------------------------------------------------------------------------
/**
* Render a single cell value as the textual form shown in tables/CSV.
* - null/undefined -> "NULL"
* - numbers/booleans -> their native string form
* - strings -> the string verbatim
* - anything else (objects/arrays) -> JSON
*
* @param {unknown} v
* @returns {string}
*/
export function formatScalar(v) {
if (v === null || v === undefined) return 'NULL';
if (typeof v === 'number' || typeof v === 'boolean') return String(v);
if (typeof v === 'string') return v;
return JSON.stringify(v);
}
/**
* Format one cell per RFC-4180-ish CSV: quote the field if it contains a
* comma, double quote, or newline; double any embedded double quotes.
*/
function csvField(v) {
const s = formatScalar(v);
if (/[,"\n\r]/.test(s)) {
return '"' + s.replace(/"/g, '""') + '"';
}
return s;
}
// ---------------------------------------------------------------------------
// Row serializers
// ---------------------------------------------------------------------------
/**
* Serialize a result set to CSV (header row + one row per record, trailing LF).
*
* @param {string[]} columns
* @param {unknown[][]} rows
* @returns {string}
*/
export function rowsToCsv(columns, rows) {
const lines = [columns.map((c) => csvField(c)).join(',')];
for (const row of rows) {
lines.push(row.map((cell) => csvField(cell)).join(','));
}
return lines.join('\n') + '\n';
}
/**
* Serialize a result set as a GitHub-flavored Markdown table. Pipes inside
* cells are escaped with a backslash so they don't break the table layout.
*
* @param {string[]} columns
* @param {unknown[][]} rows
* @returns {string}
*/
export function rowsToMarkdown(columns, rows) {
const esc = (s) => s.replace(/\|/g, '\\|');
const header = '| ' + columns.map(esc).join(' | ') + ' |';
const sep = '| ' + columns.map(() => '---').join(' | ') + ' |';
const dataRows = rows.map(
(row) => '| ' + row.map((cell) => esc(formatScalar(cell))).join(' | ') + ' |',
);
return [header, sep, ...dataRows].join('\n') + '\n';
}
/**
* Serialize a result set as a pretty-printed JSON array of objects keyed by
* column name. Missing cells (row shorter than columns) become null.
*
* @param {string[]} columns
* @param {unknown[][]} rows
* @returns {string}
*/
export function rowsToJson(columns, rows) {
const arr = rows.map((row) => {
const obj = {};
columns.forEach((col, i) => {
obj[col] = row[i] ?? null;
});
return obj;
});
return JSON.stringify(arr, null, 2);
}
// ---------------------------------------------------------------------------
// Schema summary
// ---------------------------------------------------------------------------
/**
* Pull the column name (lower-cased) from a single CREATE TABLE column
* definition. Returns null for table-level constraint lines (PRIMARY KEY,
* FOREIGN KEY, UNIQUE, CHECK, CONSTRAINT) - those are not columns.
*/
function extractColumnName(def) {
if (!def) return null;
if (/^(PRIMARY\s+KEY|FOREIGN\s+KEY|UNIQUE|CHECK|CONSTRAINT)\b/i.test(def)) {
return null;
}
const m = def.match(/^["'`]?(\w+)["'`]?/);
return m ? m[1].toLowerCase() : null;
}
/**
* Best-effort extraction of `{ table, columns }` summaries from a CREATE TABLE
* script. Handles IF NOT EXISTS, quoted identifiers, and parenthesised types
* / constraint bodies (depth-tracked so a `,` inside `NUMERIC(10,2)` doesn't
* split a column). Returns [] if anything goes wrong.
*
* @param {string} createSql
* @returns {{ table: string, columns: string[] }[]}
*/
export function summarizeSchema(createSql) {
try {
const results = [];
const headerRe = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["'`]?(\w+)["'`]?\s*\(/gi;
let match;
while ((match = headerRe.exec(createSql)) !== null) {
const table = match[1].toLowerCase();
const startIdx = match.index + match[0].length;
// Walk forward to find the matching close paren of the table body.
let depth = 1;
let i = startIdx;
while (i < createSql.length && depth > 0) {
if (createSql[i] === '(') depth++;
else if (createSql[i] === ')') depth--;
i++;
}
// Unbalanced parens -> malformed; skip this match.
if (depth !== 0) continue;
// Resume the next header search after this table's body.
headerRe.lastIndex = i;
const body = createSql.slice(startIdx, i - 1);
const columns = [];
// Split the body on top-level commas only.
let colDepth = 0;
let current = '';
for (const ch of body) {
if (ch === '(') colDepth++;
else if (ch === ')') colDepth--;
else if (ch === ',' && colDepth === 0) {
const col = extractColumnName(current.trim());
if (col) columns.push(col);
current = '';
continue;
}
current += ch;
}
// Flush the trailing column definition.
const lastCol = extractColumnName(current.trim());
if (lastCol) columns.push(lastCol);
results.push({ table, columns });
}
return results;
} catch {
return [];
}
}
Also available in 13 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →