Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions docs/guide/formatting-recipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const { formattedSql, params } = formatter.format(query);
| `valuesCommaBreak` | Same as `commaBreak` | Mirrors `commaBreak` | Comma handling within `VALUES` tuples. |
| `andBreak` | `'none'`, `'before'`, `'after'` | `'none'` | Controls whether logical `AND` operators move to their own lines. |
| `orBreak` | `'none'`, `'before'`, `'after'` | `'none'` | Same idea for logical `OR` operators. |
| `insertColumnsOneLine` | `true` / `false` | `false` | Keeps column lists inside `INSERT INTO` statements on a single line when `true`. |
| `indentNestedParentheses` | `true` / `false` | `false` | Adds an extra indent when boolean groups introduce parentheses inside `WHERE` or `HAVING` clauses. |
| `commentStyle` | `'block'`, `'smart'` | `'block'` | Normalises how comments are emitted (see below). |
| `withClauseStyle` | `'standard'`, `'cte-oneline'`, `'full-oneline'` | `'standard'` | Expands or collapses common table expressions. |
Expand Down Expand Up @@ -78,6 +79,34 @@ const formatter = new SqlFormatter({

Choose `'before'` when you want to scan down logical branches quickly, or `'after'` to keep complex conditions aligned underneath their keywords.

### INSERT column list layouts

`insertColumnsOneLine` gives you a dedicated switch for shaping `INSERT INTO` column lists without disturbing the rest of your comma settings.

- `false` (default) expands each column when you combine it with `commaBreak: 'before'` or `'after'`:
```typescript
const formatter = new SqlFormatter({
newline: 'lf',
commaBreak: 'before'
});
// insert into table_a(
// id
// , value
// )
// values ...
```
- `true` keeps the table name and columns on one line, while `valuesCommaBreak` continues to control the `VALUES` tuples:
```typescript
const formatter = new SqlFormatter({
newline: 'lf',
insertColumnsOneLine: true
});
// insert into table_a(id, value)
// values ...
```

The two insert layouts make it easy to adopt either a compact DML style or a vertically aligned style without rewriting other recipes.

### Comment style tips

Set `commentStyle: 'smart'` when you want single-line annotations to become SQL line comments (`-- like this`) while multi-line explanations are preserved as block comments. Separator banners such as `/* ===== */` stay grouped, and consecutive block comments continue to merge into a readable multi-line block.
Expand Down
167 changes: 130 additions & 37 deletions docs/public/demo/analysis-features.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
// analysis-features.js
// This module handles SQL analysis features like updating table lists, CTE lists, and schema information.

// Import rawsql-ts modules
import { SelectQueryParser, TableSourceCollector, CTECollector, SchemaCollector } from "https://unpkg.com/rawsql-ts/dist/esm/index.min.js";
// Import rawsql-ts modules from local vendor bundle for consistent class instances
import {
SqlParser,
TableSourceCollector,
CTECollector,
SchemaCollector,
MultiQuerySplitter
} from './vendor/rawsql.browser.js';

let tableListElement, cteListElement, schemaInfoEditorInstance, sqlInputElement, debounceDelayMs;

Expand All @@ -24,10 +30,63 @@ export function initAnalysisFeatures(options) {
setupSchemaInfoAutoUpdate();
}

let parseCache = { text: null, splitResult: null, statements: null, errors: null };

function getParseResult(sqlText) {
if (parseCache.text === sqlText) {
return parseCache;
}

const splitResult = MultiQuerySplitter.split(sqlText);
const statements = new Map();
const errors = [];

for (const query of splitResult.queries) {
if (query.isEmpty) {
continue;
}

try {
const ast = SqlParser.parse(query.sql);
statements.set(query.index, { ast, sql: query.sql });
} catch (error) {
errors.push({
index: query.index,
message: error instanceof Error ? error.message : String(error)
});
}
}

parseCache = { text: sqlText, splitResult, statements, errors };
return parseCache;
}

function isSelectStatement(ast) {
return Boolean(ast && typeof ast === 'object' && ast.__selectQueryType);
}

// Extract SELECT statements from the current SQL text for downstream analysis.
function extractSelectStatements(sqlText) {
const { splitResult, statements } = getParseResult(sqlText);
const selectStatements = [];

for (const query of splitResult.queries) {
if (query.isEmpty) {
continue;
}
const parsed = statements.get(query.index);
if (parsed && isSelectStatement(parsed.ast)) {
selectStatements.push(parsed);
}
}

return selectStatements;
}

// --- Table List Logic ---
function updateTableList(sqlText) {
if (!tableListElement) return;
tableListElement.innerHTML = ''; // Clear previous list
tableListElement.innerHTML = '';

if (!sqlText.trim()) {
const li = document.createElement('li');
Expand All @@ -37,28 +96,43 @@ function updateTableList(sqlText) {
}

try {
const ast = SelectQueryParser.parse(sqlText);
const collector = new TableSourceCollector(false);
const tables = collector.collect(ast);
const selectStatements = extractSelectStatements(sqlText);
if (selectStatements.length === 0) {
const li = document.createElement('li');
li.textContent = '(No SELECT statements found)';
tableListElement.appendChild(li);
return;
}

const tableNames = new Set();
for (const { ast } of selectStatements) {
const collector = new TableSourceCollector(false);
const tables = collector.collect(ast);
tables.forEach(t => {
if (t?.table?.name) {
tableNames.add(t.table.name);
}
});
}

if (tables.length === 0) {
if (tableNames.size === 0) {
const li = document.createElement('li');
li.textContent = '(No tables found)';
tableListElement.appendChild(li);
} else {
const uniqueTableNames = [...new Set(tables.map(t => t.table.name))];
uniqueTableNames.forEach(tableName => {
const li = document.createElement('li');
li.textContent = tableName;
tableListElement.appendChild(li);
});
return;
}

Array.from(tableNames).forEach(tableName => {
const li = document.createElement('li');
li.textContent = tableName;
tableListElement.appendChild(li);
});
} catch (error) {
console.error("Error parsing SQL for table list:", error);
console.error('Error parsing SQL for table list:', error);
const li = document.createElement('li');
let errorText = '(Error parsing SQL for table list)';
if (error.name === 'ParseError' && error.message) {
errorText = `(Parse Error: ${error.message.substring(0, 50)}...)`; // Keep it short
if (error?.message) {
errorText = `(Error: ${error.message.substring(0, 80)}...)`;
}
li.textContent = errorText;
tableListElement.appendChild(li);
Expand All @@ -72,7 +146,6 @@ function setupTableListAutoUpdate() {
if (tableListDebounceTimer) clearTimeout(tableListDebounceTimer);
tableListDebounceTimer = setTimeout(() => updateTableList(sqlInputElement.getValue()), debounceDelayMs);
});
// Initial population
updateTableList(sqlInputElement.getValue());
}

Expand All @@ -88,26 +161,44 @@ function updateCTEList(sqlText) {
}

try {
const query = SelectQueryParser.parse(sqlText);
const cteCollector = new CTECollector();
const ctes = cteCollector.collect(query);
if (ctes.length > 0) {
const selectStatements = extractSelectStatements(sqlText);
if (selectStatements.length === 0) {
const listItem = document.createElement('li');
listItem.textContent = '(No SELECT statements found)';
cteListElement.appendChild(listItem);
return;
}

const cteNames = new Set();
for (const { ast } of selectStatements) {
const collector = new CTECollector();
const ctes = collector.collect(ast);
ctes.forEach(cte => {
const listItem = document.createElement('li');
listItem.textContent = cte.getSourceAliasName();
cteListElement.appendChild(listItem);
const name = cte.getSourceAliasName();
if (name) {
cteNames.add(name);
}
});
} else {
}

if (cteNames.size === 0) {
const listItem = document.createElement('li');
listItem.textContent = '(No CTEs found)';
cteListElement.appendChild(listItem);
return;
}

Array.from(cteNames).forEach(name => {
const listItem = document.createElement('li');
listItem.textContent = name;
cteListElement.appendChild(listItem);
});
} catch (error) {
console.error("Error collecting CTEs:", error);
console.error('Error collecting CTEs:', error);
const listItem = document.createElement('li');
let errorText = 'Error collecting CTEs.';
if (error.name === 'ParseError' && error.message) {
errorText = `(Parse Error: ${error.message.substring(0, 50)}...)`; // Keep it short
if (error?.message) {
errorText = `(Error: ${error.message.substring(0, 80)}...)`;
}
listItem.textContent = errorText;
cteListElement.appendChild(listItem);
Expand All @@ -121,7 +212,6 @@ function setupCTEListAutoUpdate() {
if (cteListDebounceTimer) clearTimeout(cteListDebounceTimer);
cteListDebounceTimer = setTimeout(() => updateCTEList(sqlInputElement.getValue()), debounceDelayMs);
});
// Initial population
updateCTEList(sqlInputElement.getValue());
}

Expand All @@ -136,7 +226,15 @@ function updateSchemaInfo(sqlText) {
}

try {
const query = SelectQueryParser.parse(sqlText);
const selectStatements = extractSelectStatements(sqlText);
if (selectStatements.length === 0) {
schemaInfoEditorInstance.setValue('(No SELECT statements found for schema analysis)');
schemaInfoEditorInstance.refresh();
return;
}

// Focus on the first SELECT statement for schema introspection.
const query = selectStatements[0].ast;
const schemaCollector = new SchemaCollector();
const schemaInfo = schemaCollector.collect(query);

Expand All @@ -148,12 +246,7 @@ function updateSchemaInfo(sqlText) {
} catch (error) {
console.error("Error collecting schema info:", error);
let errorMessage = 'Error collecting schema info.';
if (error.name === 'ParseError' && error.message) {
errorMessage = `Error parsing SQL for schema: ${error.message}`;
if (error.details) {
errorMessage += `\nAt line ${error.details.startLine}, column ${error.details.startColumn}. Found: '${error.details.found}'`;
}
} else if (error.message) {
if (error?.message) {
errorMessage = `Error collecting schema info: ${error.message}`;
}
schemaInfoEditorInstance.setValue(errorMessage);
Expand Down
2 changes: 1 addition & 1 deletion docs/public/demo/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ <h1 style="margin: 0; font-size: 2.2rem;">rawsql-ts</h1>
</a>
</div>
<div class="project-links">
rawsql-ts enables fast parsing and formatting of SELECT queries, available on
rawsql-ts enables fast parsing and formatting of SQL statements—from SELECT queries to DML/DDL workflows—available on
<a href="https://github.com/mk3008/rawsql-ts" class="github-link" target="_blank"
rel="noopener noreferrer">GitHub</a>
and
Expand Down
Loading
Loading