From b747bf381bbfa9f47373f2ff44bba5d4e06135ac Mon Sep 17 00:00:00 2001 From: Dominik Moritz Date: Wed, 17 Sep 2025 12:21:00 -0400 Subject: [PATCH 1/4] feat: extract logic for toString into visitor --- packages/mosaic/sql/src/ast/aggregate.ts | 16 +- packages/mosaic/sql/src/ast/between-op.ts | 14 +- packages/mosaic/sql/src/ast/binary-op.ts | 7 +- packages/mosaic/sql/src/ast/case.ts | 18 +- packages/mosaic/sql/src/ast/cast.ts | 8 +- packages/mosaic/sql/src/ast/collate.ts | 7 +- packages/mosaic/sql/src/ast/column-ref.ts | 11 +- packages/mosaic/sql/src/ast/fragment.ts | 7 +- packages/mosaic/sql/src/ast/from.ts | 15 - packages/mosaic/sql/src/ast/function.ts | 8 +- packages/mosaic/sql/src/ast/in-op.ts | 7 +- packages/mosaic/sql/src/ast/interval.ts | 7 +- packages/mosaic/sql/src/ast/list.ts | 7 +- packages/mosaic/sql/src/ast/literal.ts | 7 +- packages/mosaic/sql/src/ast/logical-op.ts | 10 +- packages/mosaic/sql/src/ast/node.ts | 33 +- packages/mosaic/sql/src/ast/order-by.ts | 14 +- packages/mosaic/sql/src/ast/param.ts | 8 +- packages/mosaic/sql/src/ast/query.ts | 88 +--- packages/mosaic/sql/src/ast/sample.ts | 10 +- packages/mosaic/sql/src/ast/select.ts | 18 - packages/mosaic/sql/src/ast/subquery.ts | 7 +- packages/mosaic/sql/src/ast/table-ref.ts | 8 +- packages/mosaic/sql/src/ast/unary-op.ts | 14 +- packages/mosaic/sql/src/ast/unnest.ts | 9 +- packages/mosaic/sql/src/ast/verbatim.ts | 7 +- packages/mosaic/sql/src/ast/window-frame.ts | 33 +- packages/mosaic/sql/src/ast/window.ts | 43 +- packages/mosaic/sql/src/ast/with.ts | 11 +- packages/mosaic/sql/src/index.ts | 8 +- packages/mosaic/sql/src/init.ts | 5 + .../mosaic/sql/src/visit/duckdb-visitor.ts | 443 ++++++++++++++++++ packages/mosaic/sql/src/visit/index.ts | 6 + .../mosaic/sql/src/visit/to-string-visitor.ts | 204 ++++++++ 34 files changed, 728 insertions(+), 390 deletions(-) create mode 100644 packages/mosaic/sql/src/init.ts create mode 100644 packages/mosaic/sql/src/visit/duckdb-visitor.ts create mode 100644 packages/mosaic/sql/src/visit/index.ts create mode 100644 packages/mosaic/sql/src/visit/to-string-visitor.ts diff --git a/packages/mosaic/sql/src/ast/aggregate.ts b/packages/mosaic/sql/src/ast/aggregate.ts index 7279a20d2..bbe20c5fd 100644 --- a/packages/mosaic/sql/src/ast/aggregate.ts +++ b/packages/mosaic/sql/src/ast/aggregate.ts @@ -105,21 +105,7 @@ export class AggregateNode extends ExprNode { return this.window().frame(framedef); } - /** - * Generate a SQL query string for this node. - */ - toString() { - const { name, args, isDistinct, filter, order } = this; - const arg = [ - isDistinct ? 'DISTINCT' : '', - args?.length ? args.join(', ') - : name.toLowerCase() === 'count' ? '*' - : '', - order.length ? `ORDER BY ${order.join(', ')}` : '' - ].filter(x => x).join(' '); - const filt = filter ? ` FILTER (WHERE ${filter})` : ''; - return `${name}(${arg})${filt}`; - } + } /** diff --git a/packages/mosaic/sql/src/ast/between-op.ts b/packages/mosaic/sql/src/ast/between-op.ts index f5d67fc07..980d343ed 100644 --- a/packages/mosaic/sql/src/ast/between-op.ts +++ b/packages/mosaic/sql/src/ast/between-op.ts @@ -41,12 +41,7 @@ export class BetweenOpNode extends AbstractBetweenOpNode { super(BETWEEN_OPERATOR, expr, extent); } - /** - * Generate a SQL query string for this node. - */ - toString() { - return super.toSQL('BETWEEN'); - } + } export class NotBetweenOpNode extends AbstractBetweenOpNode { @@ -59,10 +54,5 @@ export class NotBetweenOpNode extends AbstractBetweenOpNode { super(NOT_BETWEEN_OPERATOR, expr, extent); } - /** - * Generate a SQL query string for this node. - */ - toString() { - return super.toSQL('NOT BETWEEN'); - } + } diff --git a/packages/mosaic/sql/src/ast/binary-op.ts b/packages/mosaic/sql/src/ast/binary-op.ts index ea6e6c78b..ecc77bf7f 100644 --- a/packages/mosaic/sql/src/ast/binary-op.ts +++ b/packages/mosaic/sql/src/ast/binary-op.ts @@ -22,10 +22,5 @@ export class BinaryOpNode extends ExprNode { this.right = right; } - /** - * Generate a SQL query string for this node. - */ - toString() { - return `(${this.left} ${this.op} ${this.right})`; - } + } diff --git a/packages/mosaic/sql/src/ast/case.ts b/packages/mosaic/sql/src/ast/case.ts index edcfee01d..7b481a00e 100644 --- a/packages/mosaic/sql/src/ast/case.ts +++ b/packages/mosaic/sql/src/ast/case.ts @@ -53,16 +53,7 @@ export class CaseNode extends ExprNode { return new CaseNode(this.expr, this._when, asNode(expr)); } - /** - * Generate a SQL query string for this node. - */ - toString() { - return 'CASE ' - + (this.expr ? `${this.expr} ` : '') - + this._when.join(' ') - + (this._else ? ` ELSE ${this._else}` : '') - + ' END'; - } + } export class WhenNode extends SQLNode { @@ -82,10 +73,5 @@ export class WhenNode extends SQLNode { this.then = then; } - /** - * Generate a SQL query string for this node. - */ - toString() { - return `WHEN ${this.when} THEN ${this.then}`; - } + } diff --git a/packages/mosaic/sql/src/ast/cast.ts b/packages/mosaic/sql/src/ast/cast.ts index 1f9b292cf..1ddfb1c88 100644 --- a/packages/mosaic/sql/src/ast/cast.ts +++ b/packages/mosaic/sql/src/ast/cast.ts @@ -18,11 +18,5 @@ export class CastNode extends ExprNode { this.cast = type; } - /** - * Generate a SQL query string for this node. - */ - toString() { - // TODO? could include check to see if parens are necessary - return `(${this.expr})::${this.cast}`; - } + } diff --git a/packages/mosaic/sql/src/ast/collate.ts b/packages/mosaic/sql/src/ast/collate.ts index bb46a0151..aee52b823 100644 --- a/packages/mosaic/sql/src/ast/collate.ts +++ b/packages/mosaic/sql/src/ast/collate.ts @@ -18,10 +18,5 @@ export class CollateNode extends ExprNode { this.collation = collation; } - /** - * Generate a SQL query string for this node. - */ - toString() { - return `${this.expr} ${COLLATE} ${this.collation}`; - } + } diff --git a/packages/mosaic/sql/src/ast/column-ref.ts b/packages/mosaic/sql/src/ast/column-ref.ts index 1d3d30caa..e3d654b12 100644 --- a/packages/mosaic/sql/src/ast/column-ref.ts +++ b/packages/mosaic/sql/src/ast/column-ref.ts @@ -1,6 +1,5 @@ import type { TableRefNode } from './table-ref.js'; import { COLUMN_REF } from '../constants.js'; -import { quoteIdentifier } from '../util/string.js'; import { ExprNode } from './node.js'; /** @@ -32,15 +31,7 @@ export class ColumnRefNode extends ExprNode { return ''; // subclasses to override } - /** - * Generate a SQL query string for this node. - */ - toString() { - const { column, table } = this; - const tref = `${table ?? ''}`; - const id = column === '*' ? '*' : quoteIdentifier(column); - return (tref ? (tref + '.') : '') + id; - } + } export class ColumnNameRefNode extends ColumnRefNode { diff --git a/packages/mosaic/sql/src/ast/fragment.ts b/packages/mosaic/sql/src/ast/fragment.ts index 43eb273fa..1fc30289c 100644 --- a/packages/mosaic/sql/src/ast/fragment.ts +++ b/packages/mosaic/sql/src/ast/fragment.ts @@ -19,10 +19,5 @@ export class FragmentNode extends ExprNode { this.spans = spans; } - /** - * Generate a SQL query string for this node. - */ - toString() { - return this.spans.join(''); - } + } diff --git a/packages/mosaic/sql/src/ast/from.ts b/packages/mosaic/sql/src/ast/from.ts index 578be30fb..a50d7a4f0 100644 --- a/packages/mosaic/sql/src/ast/from.ts +++ b/packages/mosaic/sql/src/ast/from.ts @@ -1,9 +1,6 @@ import type { SampleClauseNode } from './sample.js'; import { FROM_CLAUSE } from '../constants.js'; -import { quoteIdentifier } from '../util/string.js'; import { SQLNode } from './node.js'; -import { isQuery } from './query.js'; -import { isTableRef } from './table-ref.js'; /** * AST node corresponding to a table source which can appear @@ -40,16 +37,4 @@ export class FromClauseNode extends FromNode { as(alias: string) { return new FromClauseNode(this.expr, alias, this.sample); } - - /** - * Generate a SQL query string for this node. - */ - toString() { - const { expr, alias, sample } = this; - const ref = isQuery(expr) ? `(${expr})` : `${expr}`; - const from = alias && !(isTableRef(expr) && expr.table.join('.') === alias) - ? `${ref} AS ${quoteIdentifier(alias)}` - : `${ref}`; - return `${from}${sample ? ` TABLESAMPLE ${sample}` : ''}`; - } } diff --git a/packages/mosaic/sql/src/ast/function.ts b/packages/mosaic/sql/src/ast/function.ts index d73b3273e..fdc751503 100644 --- a/packages/mosaic/sql/src/ast/function.ts +++ b/packages/mosaic/sql/src/ast/function.ts @@ -18,11 +18,5 @@ export class FunctionNode extends ExprNode { this.args = args; } - /** - * Generate a SQL query string for this node. - */ - toString() { - const { name, args } = this; - return `${name}(${args.join(', ')})`; - } + } diff --git a/packages/mosaic/sql/src/ast/in-op.ts b/packages/mosaic/sql/src/ast/in-op.ts index 577efbf18..deabf24c5 100644 --- a/packages/mosaic/sql/src/ast/in-op.ts +++ b/packages/mosaic/sql/src/ast/in-op.ts @@ -18,10 +18,5 @@ export class InOpNode extends ExprNode { this.values = values; } - /** - * Generate a SQL query string for this node. - */ - toString() { - return `(${this.expr} IN (${this.values.join(', ')}))`; - } + } diff --git a/packages/mosaic/sql/src/ast/interval.ts b/packages/mosaic/sql/src/ast/interval.ts index 6c849e924..1e8f9678e 100644 --- a/packages/mosaic/sql/src/ast/interval.ts +++ b/packages/mosaic/sql/src/ast/interval.ts @@ -18,10 +18,5 @@ export class IntervalNode extends ExprNode { this.steps = steps; } - /** - * Generate a SQL query string for this node. - */ - toString() { - return `INTERVAL ${this.steps} ${this.name}`; - } + } diff --git a/packages/mosaic/sql/src/ast/list.ts b/packages/mosaic/sql/src/ast/list.ts index 768973aaf..91b2fc7dd 100644 --- a/packages/mosaic/sql/src/ast/list.ts +++ b/packages/mosaic/sql/src/ast/list.ts @@ -14,10 +14,5 @@ export class ListNode extends ExprNode { this.values = values; } - /** - * Generate a SQL query string for this node. - */ - toString() { - return `[${this.values.join(', ')}]`; - } + } diff --git a/packages/mosaic/sql/src/ast/literal.ts b/packages/mosaic/sql/src/ast/literal.ts index 7882723d3..e48b956fd 100644 --- a/packages/mosaic/sql/src/ast/literal.ts +++ b/packages/mosaic/sql/src/ast/literal.ts @@ -14,12 +14,7 @@ export class LiteralNode extends ExprNode { this.value = value; } - /** - * Generate a SQL query string for this node. - */ - toString() { - return literalToSQL(this.value); - } + } export function literalToSQL(value: unknown) { diff --git a/packages/mosaic/sql/src/ast/logical-op.ts b/packages/mosaic/sql/src/ast/logical-op.ts index 6cb4b475c..79e2ba2f9 100644 --- a/packages/mosaic/sql/src/ast/logical-op.ts +++ b/packages/mosaic/sql/src/ast/logical-op.ts @@ -18,15 +18,7 @@ export class LogicalOpNode extends ExprNode { this.clauses = clauses; } - /** - * Generate a SQL query string for this node. - */ - toString() { - const c = this.clauses; - return c.length === 0 ? '' - : c.length === 1 ? `${c[0]}` - : `(${c.join(` ${this.op} `)})`; - } + } export class AndNode extends LogicalOpNode { diff --git a/packages/mosaic/sql/src/ast/node.ts b/packages/mosaic/sql/src/ast/node.ts index 74fc0904c..39dfbf640 100644 --- a/packages/mosaic/sql/src/ast/node.ts +++ b/packages/mosaic/sql/src/ast/node.ts @@ -1,3 +1,5 @@ +import type { BaseToStringVisitor } from '../visit/to-string-visitor.js'; + /** * Check if a value is a SQL AST node. * @param value The value to check. @@ -6,6 +8,16 @@ export function isNode(value: unknown): value is SQLNode { return value instanceof SQLNode; } +let _defaultVisitor: BaseToStringVisitor | null = null; + +/** + * Set the default visitor for toString operations. + * This is used when no visitor is explicitly provided. + */ +export function setDefaultVisitor(visitor: BaseToStringVisitor) { + _defaultVisitor = visitor; +} + export class SQLNode { /** The SQL AST node type. */ readonly type: string; @@ -23,13 +35,28 @@ export class SQLNode { * @returns The shallow clone node. */ clone(): this { - // @ts-expect-error use constructor - const clone = new this.constructor(); + // @ts-expect-error use constructor with type + const clone = new this.constructor(this.type); for (const key in this) { - clone[key] = this[key]; + if (key !== 'type') { // Skip type since it's already set by constructor + clone[key] = this[key]; + } } return clone; } + + /** + * Generate a SQL query string for this node using a specific dialect visitor. + * @param visitor Optional SQL visitor to use for string generation. If not provided, uses the default visitor. + * @returns The SQL string representation. + */ + toString(visitor?: BaseToStringVisitor): string { + const visitorToUse = visitor || _defaultVisitor; + if (!visitorToUse) { + throw new Error('No visitor provided and no default visitor set.'); + } + return visitorToUse.toString(this); + } } /** diff --git a/packages/mosaic/sql/src/ast/order-by.ts b/packages/mosaic/sql/src/ast/order-by.ts index c928e4b46..22f0566db 100644 --- a/packages/mosaic/sql/src/ast/order-by.ts +++ b/packages/mosaic/sql/src/ast/order-by.ts @@ -22,17 +22,5 @@ export class OrderByNode extends ExprNode { this.nullsFirst = nullsFirst; } - /** - * Generate a SQL query string for this node. - */ - toString() { - const { expr, desc, nullsFirst } = this; - const dir = desc ? ' DESC' - : desc === false ? ' ASC' - : ''; - const nf = nullsFirst ? ' NULLS FIRST' - : nullsFirst === false ? ' NULLS LAST' - : ''; - return `${expr}${dir}${nf}`; - } + } diff --git a/packages/mosaic/sql/src/ast/param.ts b/packages/mosaic/sql/src/ast/param.ts index 429c2b1a7..621df8356 100644 --- a/packages/mosaic/sql/src/ast/param.ts +++ b/packages/mosaic/sql/src/ast/param.ts @@ -1,6 +1,5 @@ import type { ParamLike } from '../types.js'; import { PARAM } from '../constants.js'; -import { literalToSQL } from './literal.js'; import { ExprNode } from './node.js'; export class ParamNode extends ExprNode { @@ -23,10 +22,5 @@ export class ParamNode extends ExprNode { return this.param.value; } - /** - * Generate a SQL query string for this node. - */ - toString() { - return literalToSQL(this.value); - } + } diff --git a/packages/mosaic/sql/src/ast/query.ts b/packages/mosaic/sql/src/ast/query.ts index 3c80c2998..9a9dea70d 100644 --- a/packages/mosaic/sql/src/ast/query.ts +++ b/packages/mosaic/sql/src/ast/query.ts @@ -455,65 +455,7 @@ export class SelectQuery extends Query { return this; } - /** - * Generate a SQL query string. - */ - toString() { - const { - _with, _select, _distinct, _from, _sample, _where, _groupby, - _having, _window, _qualify, _orderby, _limitPerc, _limit, _offset - } = this; - const sql = []; - - // WITH - if (_with.length) sql.push(`WITH ${_with.join(', ')}`); - - // SELECT - sql.push(`SELECT${_distinct ? ' DISTINCT' : ''} ${_select.join(', ')}`); - - // FROM - if (_from.length) sql.push(`FROM ${_from.join(', ')}`); - - // WHERE - if (_where.length) { - const clauses = _where.map(String).filter(x => x).join(' AND '); - if (clauses) sql.push(`WHERE ${clauses}`); - } - - // SAMPLE - if (_sample) sql.push(`USING SAMPLE ${_sample}`); - - // GROUP BY - if (_groupby.length) { - sql.push(`GROUP BY ${_groupby.join(', ')}`); - } - - // HAVING - if (_having.length) { - const clauses = _having.map(String).filter(x => x).join(' AND '); - if (clauses) sql.push(`HAVING ${clauses}`); - } - - // WINDOW - if (_window.length) sql.push(`WINDOW ${_window.join(', ')}`); - - // QUALIFY - if (_qualify.length) { - const clauses = _qualify.map(String).filter(x => x).join(' AND '); - if (clauses) sql.push(`QUALIFY ${clauses}`); - } - - // ORDER BY - if (_orderby.length) sql.push(`ORDER BY ${_orderby.join(', ')}`); - // LIMIT - if (_limit) sql.push(`LIMIT ${_limit}${_limitPerc ? '%' : ''}`); - - // OFFSET - if (_offset) sql.push(`OFFSET ${_offset}`); - - return sql.join(' '); - } } export class DescribeQuery extends SQLNode { @@ -536,12 +478,7 @@ export class DescribeQuery extends SQLNode { return new DescribeQuery(this.query.clone()); } - /** - * Generate a SQL query string. - */ - toString() { - return `DESCRIBE ${this.query}`; - } + } export class SetOperation extends Query { @@ -587,30 +524,7 @@ export class SetOperation extends Query { return Object.assign(new SetOperation(op, queries), rest); } - /** - * Generate a SQL query string. - */ - toString() { - const { op, queries, _with, _orderby, _limitPerc, _limit, _offset } = this; - const sql = []; - - // WITH - if (_with.length) sql.push(`WITH ${_with.join(', ')}`); - - // SUBQUERIES - sql.push(queries.join(` ${op} `)); - - // ORDER BY - if (_orderby.length) sql.push(`ORDER BY ${_orderby.join(', ')}`); - // LIMIT - if (_limit) sql.push(`LIMIT ${_limit}${_limitPerc ? '%' : ''}`); - - // OFFSET - if (_offset) sql.push(`OFFSET ${_offset}`); - - return sql.join(' '); - } } class WithClause { diff --git a/packages/mosaic/sql/src/ast/sample.ts b/packages/mosaic/sql/src/ast/sample.ts index 351e2c23c..1d262909b 100644 --- a/packages/mosaic/sql/src/ast/sample.ts +++ b/packages/mosaic/sql/src/ast/sample.ts @@ -35,13 +35,5 @@ export class SampleClauseNode extends SQLNode { this.seed = seed; } - /** - * Generate a SQL query string for this node. - */ - toString() { - const { size, perc, method, seed } = this; - const m = method ? `${method} ` : ''; - const s = seed != null ? ` REPEATABLE (${seed})` : ''; - return `${m}(${size}${perc ? '%' : ' ROWS'})${s}`; - } + } diff --git a/packages/mosaic/sql/src/ast/select.ts b/packages/mosaic/sql/src/ast/select.ts index 5381ff389..b397609d0 100644 --- a/packages/mosaic/sql/src/ast/select.ts +++ b/packages/mosaic/sql/src/ast/select.ts @@ -1,7 +1,5 @@ import type { ExprNode } from './node.js'; import { SELECT_CLAUSE } from '../constants.js'; -import { quoteIdentifier } from '../util/string.js'; -import { ColumnRefNode } from './column-ref.js'; import { SQLNode } from './node.js'; export class SelectClauseNode extends SQLNode { @@ -20,20 +18,4 @@ export class SelectClauseNode extends SQLNode { this.expr = expr; this.alias = alias; } - - /** - * Generate a SQL query string for this node. - */ - toString() { - const { expr, alias } = this; - return !alias || isColumnRefFor(expr, alias) - ? `${expr}` - : `${expr} AS ${quoteIdentifier(alias)}`; - } -} - -function isColumnRefFor(expr: unknown, name: string): expr is ColumnRefNode { - return expr instanceof ColumnRefNode - && expr.table == null - && expr.column === name; } diff --git a/packages/mosaic/sql/src/ast/subquery.ts b/packages/mosaic/sql/src/ast/subquery.ts index def740a00..9d02dbd95 100644 --- a/packages/mosaic/sql/src/ast/subquery.ts +++ b/packages/mosaic/sql/src/ast/subquery.ts @@ -15,10 +15,5 @@ export class ScalarSubqueryNode extends ExprNode { this.subquery = subquery; } - /** - * Generate a SQL query string for this node. - */ - toString() { - return `(${this.subquery})`; - } + } diff --git a/packages/mosaic/sql/src/ast/table-ref.ts b/packages/mosaic/sql/src/ast/table-ref.ts index afae8d8ec..8437f1735 100644 --- a/packages/mosaic/sql/src/ast/table-ref.ts +++ b/packages/mosaic/sql/src/ast/table-ref.ts @@ -1,5 +1,4 @@ import { TABLE_REF } from '../constants.js'; -import { quoteIdentifier } from '../util/string.js'; import { ExprNode } from './node.js'; /** @@ -30,10 +29,5 @@ export class TableRefNode extends ExprNode { return this.table[this.table.length - 1]; } - /** - * Generate a SQL query string for this node. - */ - toString() { - return this.table.map(t => quoteIdentifier(t)).join('.'); - } + } diff --git a/packages/mosaic/sql/src/ast/unary-op.ts b/packages/mosaic/sql/src/ast/unary-op.ts index 151c6a321..01b6c9959 100644 --- a/packages/mosaic/sql/src/ast/unary-op.ts +++ b/packages/mosaic/sql/src/ast/unary-op.ts @@ -30,12 +30,7 @@ export class UnaryOpNode extends AbstractUnaryOpNode { super(UNARY_OPERATOR, op, expr); } - /** - * Generate a SQL query string for this node. - */ - toString() { - return `(${this.op} ${this.expr})`; - } + } export class UnaryPostfixOpNode extends AbstractUnaryOpNode { @@ -48,10 +43,5 @@ export class UnaryPostfixOpNode extends AbstractUnaryOpNode { super(UNARY_POSTFIX_OPERATOR, op, expr); } - /** - * Generate a SQL query string for this node. - */ - toString() { - return `(${this.expr} ${this.op})`; - } + } diff --git a/packages/mosaic/sql/src/ast/unnest.ts b/packages/mosaic/sql/src/ast/unnest.ts index b037461b5..3b57bd14d 100644 --- a/packages/mosaic/sql/src/ast/unnest.ts +++ b/packages/mosaic/sql/src/ast/unnest.ts @@ -20,12 +20,5 @@ export class UnnestNode extends ExprNode { this.maxDepth = maxDepth; } - /** - * Generate a SQL query string for this node. - */ - toString() { - const recursive = this.recursive ? ', recursive := true' : ''; - const maxDepth = this.maxDepth > 0 ? `, max_depth := ${this.maxDepth}` : ''; - return `UNNEST(${this.expr}${recursive}${maxDepth})`; - } + } diff --git a/packages/mosaic/sql/src/ast/verbatim.ts b/packages/mosaic/sql/src/ast/verbatim.ts index 986b7ead4..105ba7784 100644 --- a/packages/mosaic/sql/src/ast/verbatim.ts +++ b/packages/mosaic/sql/src/ast/verbatim.ts @@ -18,10 +18,5 @@ export class VerbatimNode extends ExprNode { this.hint = hint; } - /** - * Generate a SQL query string for this node. - */ - toString() { - return this.value; - } + } diff --git a/packages/mosaic/sql/src/ast/window-frame.ts b/packages/mosaic/sql/src/ast/window-frame.ts index 61fbf17a0..23d19dc31 100644 --- a/packages/mosaic/sql/src/ast/window-frame.ts +++ b/packages/mosaic/sql/src/ast/window-frame.ts @@ -1,7 +1,7 @@ import { WINDOW_EXTENT_EXPR, WINDOW_FRAME } from '../constants.js'; import { type ParamLike } from '../types.js'; import { isParamLike } from '../util/type-check.js'; -import { type ExprNode, isNode, SQLNode } from './node.js'; +import { type ExprNode, SQLNode } from './node.js'; import { ParamNode } from './param.js'; export type FrameValue = ExprNode | number | null; @@ -42,27 +42,6 @@ export class WindowFrameNode extends SQLNode { this.extent = isParamLike(extent) ? new ParamNode(extent) : extent; this.exclude = exclude; } - - /** - * Generate a SQL query string for this node. - */ - toString() { - const { frameType, exclude, extent } = this; - const [prev, next] = isNode(extent) - ? extent.value as [unknown, unknown] - : extent; - const a = asFrameExpr(prev, PRECEDING); - const b = asFrameExpr(next, FOLLOWING); - return `${frameType} BETWEEN ${a} AND ${b}${exclude ? ` ${exclude}` : ''}`; - } -} - -function asFrameExpr(value: unknown, scope: string) { - return value instanceof WindowFrameExprNode ? value - : value != null && typeof value !== 'number' ? `${value} ${scope}` - : value === 0 ? CURRENT_ROW - : !(value && Number.isFinite(value)) ? `${UNBOUNDED} ${scope}` - : `${Math.abs(value)} ${scope}`; } export class WindowFrameExprNode extends SQLNode { @@ -85,13 +64,5 @@ export class WindowFrameExprNode extends SQLNode { this.expr = expr; } - /** - * Generate a SQL query string for this node. - */ - toString() { - const { scope, expr } = this; - return scope === CURRENT_ROW - ? scope - : `${expr ?? UNBOUNDED} ${scope}`; - } + } diff --git a/packages/mosaic/sql/src/ast/window.ts b/packages/mosaic/sql/src/ast/window.ts index 11a1c4bbb..c89113024 100644 --- a/packages/mosaic/sql/src/ast/window.ts +++ b/packages/mosaic/sql/src/ast/window.ts @@ -3,7 +3,6 @@ import type { AggregateNode } from './aggregate.js'; import type { WindowFrameNode } from './window-frame.js'; import { WINDOW, WINDOW_CLAUSE, WINDOW_DEF, WINDOW_FUNCTION } from '../constants.js'; import { nodeList } from '../util/function.js'; -import { quoteIdentifier } from '../util/string.js'; import { ExprNode, SQLNode } from './node.js'; export class WindowClauseNode extends SQLNode { @@ -23,12 +22,7 @@ export class WindowClauseNode extends SQLNode { this.def = def; } - /** - * Generate a SQL query string for this node. - */ - toString() { - return `${quoteIdentifier(this.name)} AS ${this.def}`; - } + } export class WindowNode extends ExprNode { @@ -85,12 +79,7 @@ export class WindowNode extends ExprNode { return new WindowNode(this.func, this.def.frame(framedef)); } - /** - * Generate a SQL query string for this node. - */ - toString() { - return `${this.func} OVER ${this.def}`; - } + } export class WindowFunctionNode extends ExprNode { @@ -124,18 +113,7 @@ export class WindowFunctionNode extends ExprNode { this.order = nodeList([argOrder]); } - /** - * Generate a SQL query string for this node. - */ - toString() { - const { name, args, ignoreNulls, order } = this; - const arg = [ - args.join(', '), - order.length ? `ORDER BY ${order.join(', ')}` : '', - ignoreNulls ? 'IGNORE NULLS' : '' - ].filter(x => x).join(' '); - return `${name}(${arg})`; - } + } export class WindowDefNode extends SQLNode { @@ -204,20 +182,7 @@ export class WindowDefNode extends SQLNode { return deriveDef(this, { framedef }); } - /** - * Generate a SQL query string for this node. - */ - toString() { - const { name, partition, order, framedef } = this; - const base = name && quoteIdentifier(name); - const def = [ - base, - partition?.length && `PARTITION BY ${partition.join(', ')}`, - order?.length && `ORDER BY ${order.join(', ')}`, - framedef - ].filter(x => x); - return base && def.length < 2 ? base : `(${def.join(' ')})`; - } + } interface DeriveDefOptions { diff --git a/packages/mosaic/sql/src/ast/with.ts b/packages/mosaic/sql/src/ast/with.ts index 260fdcd47..ec47f42e8 100644 --- a/packages/mosaic/sql/src/ast/with.ts +++ b/packages/mosaic/sql/src/ast/with.ts @@ -30,14 +30,5 @@ export class WithClauseNode extends SQLNode { this.materialized = materialized; } - /** - * Generate a SQL query string for this node. - */ - toString() { - const flag = this.materialized; - const mat = flag === true ? ' MATERIALIZED' - : flag === false ? ' NOT MATERIALIZED' - : ''; - return `"${this.name}" AS${mat} (${this.query})`; - } + } diff --git a/packages/mosaic/sql/src/index.ts b/packages/mosaic/sql/src/index.ts index 9b78897f6..2079c36b1 100644 --- a/packages/mosaic/sql/src/index.ts +++ b/packages/mosaic/sql/src/index.ts @@ -1,3 +1,6 @@ +// Initialize default visitor +import './init.js'; + export { AggregateNode, aggregateNames, isAggregateFunction } from './ast/aggregate.js'; export { BetweenOpNode, NotBetweenOpNode } from './ast/between-op.js' export { BinaryOpNode } from './ast/binary-op.js'; @@ -14,7 +17,7 @@ export { IntervalNode } from './ast/interval.js'; export { JoinNode, type JoinType, type JoinVariant } from './ast/join.js'; export { ListNode } from './ast/list.js'; export { LiteralNode } from './ast/literal.js'; -export { AndNode, OrNode } from './ast/logical-op.js'; +export { LogicalOpNode, AndNode, OrNode } from './ast/logical-op.js'; export { SQLNode, ExprNode, isNode } from './ast/node.js'; export { OrderByNode } from './ast/order-by.js'; export { ParamNode } from './ast/param.js'; @@ -57,6 +60,9 @@ export { deepClone } from './visit/clone.js'; export { rewrite } from './visit/rewrite.js'; export { collectAggregates, collectColumns, collectParams, isAggregateExpression } from './visit/visitors.js'; export { walk, type VisitorCallback, type VisitorResult } from './visit/walk.js'; +export { ToStringVisitor } from './visit/to-string-visitor.js'; +export { DuckDBVisitor, duckdbVisitor } from './visit/duckdb-visitor.js'; +export { defaultVisitor } from './visit/index.js'; export { createTable, createSchema } from './load/create.js'; export { loadExtension } from './load/extension.js'; diff --git a/packages/mosaic/sql/src/init.ts b/packages/mosaic/sql/src/init.ts new file mode 100644 index 000000000..3497dabfd --- /dev/null +++ b/packages/mosaic/sql/src/init.ts @@ -0,0 +1,5 @@ +import { setDefaultVisitor } from './ast/node.js'; +import { duckdbVisitor } from './visit/duckdb-visitor.js'; + +// Initialize the default visitor +setDefaultVisitor(duckdbVisitor); diff --git a/packages/mosaic/sql/src/visit/duckdb-visitor.ts b/packages/mosaic/sql/src/visit/duckdb-visitor.ts new file mode 100644 index 000000000..dc254e6ca --- /dev/null +++ b/packages/mosaic/sql/src/visit/duckdb-visitor.ts @@ -0,0 +1,443 @@ +import { ToStringVisitor } from './to-string-visitor.js'; +import { + SQLNode, + ExprNode, + AggregateNode, + BetweenOpNode, + NotBetweenOpNode, + BinaryOpNode, + CaseNode, + WhenNode, + CastNode, + CollateNode, + ColumnParamNode, + ColumnRefNode, + FragmentNode, + FromClauseNode, + FunctionNode, + InOpNode, + IntervalNode, + ListNode, + LiteralNode, + LogicalOpNode, + OrderByNode, + ParamNode, + DescribeQuery, + SelectQuery, + SetOperation, + SampleClauseNode, + SelectClauseNode, + ScalarSubqueryNode, + TableRefNode, + UnaryOpNode, + UnaryPostfixOpNode, + UnnestNode, + VerbatimNode, + WindowNode, + WindowClauseNode, + WindowDefNode, + WindowFunctionNode, + WindowFrameNode, + WindowFrameExprNode, + WithClauseNode +} from '../index.js'; +import { quoteIdentifier } from '../util/string.js'; +import { literalToSQL } from '../ast/literal.js'; + +function betweenToString(node: BetweenOpNode | NotBetweenOpNode, op: string) { + const { extent: r, expr } = node; + return r ? `(${expr} ${op} ${r[0]} AND ${r[1]})` : ''; +} + + +function isColumnRefFor(expr: unknown, name: string): expr is ColumnRefNode { + return expr instanceof ColumnRefNode + && expr.table == null + && expr.column === name; +} + + +/** + * DuckDB SQL dialect visitor for converting AST nodes to DuckDB-compatible SQL. + */ +export class DuckDBVisitor extends ToStringVisitor { + visitAggregate(node: AggregateNode): string { + const { name, args, isDistinct, filter, order } = node; + const arg = [ + isDistinct ? 'DISTINCT' : '', + args?.length ? this.mapToString(args).join(', ') + : name.toLowerCase() === 'count' ? '*' + : '', + order.length ? `ORDER BY ${this.mapToString(order).join(', ')}` : '' + ].filter(x => x).join(' '); + const filt = filter ? ` FILTER (WHERE ${this.toString(filter)})` : ''; + return `${name}(${arg})${filt}`; + } + + visitBetween(node: BetweenOpNode): string { + return betweenToString(node, 'BETWEEN'); + } + + visitBinary(node: BinaryOpNode): string { + const { left, right, op } = node; + return `(${this.toString(left)} ${op} ${this.toString(right)})`; + } + + visitCase(node: CaseNode): string { + const { expr, _when, _else } = node; + return 'CASE ' + + (expr ? `${this.toString(expr)} ` : '') + + this.mapToString(_when).join(' ') + + (_else ? ` ELSE ${this.toString(_else)}` : '') + + ' END'; + } + + visitCast(node: CastNode): string { + const { expr, cast } = node; + return `(${this.toString(expr)})::${cast}`; + } + + visitCollate(node: CollateNode): string { + const { expr, collation } = node; + return `${this.toString(expr)} COLLATE ${collation}`; + } + + visitColumnParam(node: ColumnParamNode): string { + const { param, table } = node; + const tref = table ? `${this.toString(table)}.` : ''; + return `${tref}${this.toString(param)}`; + } + + visitColumnRef(node: ColumnRefNode): string { + const { column, table } = node; + const tref = table ? `${this.toString(table)}.` : ''; + const id = column === '*' ? '*' : quoteIdentifier(column); + return `${tref}${id}`; + } + + visitDescribeQuery(node: DescribeQuery): string { + const { query } = node; + return `DESCRIBE ${this.toString(query)}`; + } + + visitExpression(node: ExprNode): string { + // This method might not be used in practice, but needs to exist for the interface + // If we reach here, it might be an error or a generic fallback + throw new Error(`Unexpected EXPRESSION node type. Node: ${JSON.stringify(node)}`); + } + + visitFragment(node: FragmentNode): string { + const { spans } = node; + return spans.map((span: string | SQLNode) => { + return typeof span === 'string' ? span : this.toString(span); + }).join(''); + } + + visitFromClause(node: FromClauseNode): string { + const { expr, alias, sample } = node; + const isQuery = expr.type === 'SELECT_QUERY' || expr.type === 'SET_OPERATION'; + const isTableRef = expr.type === 'TABLE_REF'; + const ref = isQuery ? `(${this.toString(expr)})` : `${this.toString(expr)}`; + const from = alias && !(isTableRef && (expr as any).table?.join('.') === alias) + ? `${ref} AS ${quoteIdentifier(alias)}` + : `${ref}`; + return `${from}${sample ? ` TABLESAMPLE ${this.toString(sample)}` : ''}`; + } + + visitFunction(node: FunctionNode): string { + const { name, args } = node; + return `${name}(${this.mapToString(args).join(', ')})`; + } + + visitIn(node: InOpNode): string { + const { expr, values } = node; + return `(${this.toString(expr)} IN (${this.mapToString(values).join(', ')}))`; + } + + visitInterval(node: IntervalNode): string { + const { steps, name } = node; + return `INTERVAL ${steps} ${name}`; + } + + visitList(node: ListNode): string { + const { values } = node; + return `[${this.mapToString(values).join(', ')}]`; + } + + visitLiteral(node: LiteralNode): string { + const { value } = node; + return literalToSQL(value); + } + + visitLogicalOperator(node: LogicalOpNode): string { + const { clauses, op } = node; + const c = this.mapToString(clauses); + return c.length === 0 ? '' + : c.length === 1 ? `${c[0]}` + : `(${c.join(` ${op} `)})`; + } + + visitNotBetween(node: NotBetweenOpNode): string { + return betweenToString(node, 'NOT BETWEEN'); + } + + visitOrderBy(node: OrderByNode): string { + const { expr, desc, nullsFirst } = node; + const dir = desc ? ' DESC' + : desc === false ? ' ASC' + : ''; + const nf = nullsFirst ? ' NULLS FIRST' + : nullsFirst === false ? ' NULLS LAST' + : ''; + return `${this.toString(expr)}${dir}${nf}`; + } + + visitParam(node: ParamNode): string { + const { param } = node; + // Get the current value from the parameter and format it as a literal + return literalToSQL(param.value); + } + + visitSampleClause(node: SampleClauseNode): string { + const { size, perc, method, seed } = node; + const m = method ? `${method} ` : ''; + const s = seed != null ? ` REPEATABLE (${seed})` : ''; + return `${m}(${size}${perc ? '%' : ' ROWS'})${s}`; + } + + visitScalarSubquery(node: ScalarSubqueryNode): string { + return `(${this.toString(node.subquery)})`; + } + + visitSelectClause(node: SelectClauseNode): string { + const { expr, alias } = node; + + return !alias || isColumnRefFor(expr, alias) + ? this.toString(expr) + : `${this.toString(expr)} AS ${quoteIdentifier(alias)}`; + } + + visitSelectQuery(node: SelectQuery): string { + const { + _with, _select, _distinct, _from, _sample, _where, + _groupby, _having, _window, _qualify, _orderby, + _limitPerc, _limit, _offset + } = node; + + const sql = []; + + // WITH + if (_with.length) { + sql.push(`WITH ${this.mapToString(_with).join(', ')}`); + } + + // SELECT + sql.push(`SELECT${_distinct ? ' DISTINCT' : ''} ${this.mapToString(_select).join(', ')}`); + + // FROM + if (_from.length) { + sql.push(`FROM ${this.mapToString(_from).join(', ')}`); + } + + // SAMPLE + if (_sample) { + sql.push(`USING SAMPLE ${this.toString(_sample)}`); + } + + // WHERE + if (_where.length) { + const clauses = this.mapToString(_where).filter(x => x).join(' AND '); + if (clauses) sql.push(`WHERE ${clauses}`); + } + + // GROUP BY + if (_groupby.length) { + sql.push(`GROUP BY ${this.mapToString(_groupby).join(', ')}`); + } + + // HAVING + if (_having.length) { + const clauses = this.mapToString(_having).filter(x => x).join(' AND '); + if (clauses) sql.push(`HAVING ${clauses}`); + } + + // WINDOW + if (_window.length) { + sql.push(`WINDOW ${this.mapToString(_window).join(', ')}`); + } + + // QUALIFY + if (_qualify.length) { + const clauses = this.mapToString(_qualify).filter(x => x).join(' AND '); + if (clauses) sql.push(`QUALIFY ${clauses}`); + } + + // ORDER BY + if (_orderby.length) { + sql.push(`ORDER BY ${this.mapToString(_orderby).join(', ')}`); + } + + // LIMIT + if (_limit) { + sql.push(`LIMIT ${this.toString(_limit)}${_limitPerc ? '%' : ''}`); + } + + // OFFSET + if (_offset != null) { + sql.push(`OFFSET ${this.toString(_offset)}`); + } + + return sql.join(' '); + } + + visitSetOperation(node: SetOperation): string { + const { queries, op, _orderby } = node; + let sql = this.mapToString(queries).join(` ${op} `); + if (_orderby.length) { + sql += ` ORDER BY ${this.mapToString(_orderby).join(', ')}`; + } + return sql; + } + + visitTableRef(node: TableRefNode): string { + const { table } = node; + return table.map((t: string) => quoteIdentifier(t)).join('.'); + } + + visitUnary(node: UnaryOpNode): string { + const { expr, op } = node; + return `(${op} ${this.toString(expr)})`; + } + + visitUnaryPostfix(node: UnaryPostfixOpNode): string { + const { expr, op } = node; + return `(${this.toString(expr)} ${op})`; + } + + visitUnnest(node: UnnestNode): string { + const { expr, recursive, maxDepth } = node; + const args = [this.toString(expr)]; + + if (recursive) { + args.push('recursive := true'); + } + if (maxDepth != null && maxDepth > 0) { + args.push(`max_depth := ${maxDepth}`); + } + + return `UNNEST(${args.join(', ')})`; + } + + visitVerbatim(node: VerbatimNode): string { + const { value } = node; + return value; + } + + visitWhen(node: WhenNode): string { + const { when, then } = node; + return `WHEN ${this.toString(when)} THEN ${this.toString(then)}`; + } + + visitWindow(node: WindowNode): string { + const { func, def } = node; + return `${this.toString(func)} OVER ${this.toString(def)}`; + } + + visitWindowClause(node: WindowClauseNode): string { + const { name, def } = node; + return `${quoteIdentifier(name)} AS ${this.toString(def)}`; + } + + visitWindowDef(node: WindowDefNode): string { + const { name, partition, order, framedef } = node; + const base = name && quoteIdentifier(name); + const def = [ + base, + partition?.length && `PARTITION BY ${this.mapToString(partition).join(', ')}`, + order?.length && `ORDER BY ${this.mapToString(order).join(', ')}`, + framedef && this.toString(framedef) + ].filter(x => x); + return base && def.length < 2 ? base : `(${def.join(' ')})`; + } + + visitWindowExtentExpr(node: WindowFrameExprNode): string { + const { scope, expr } = node; + if (expr === null || expr === undefined) { + return scope; // e.g., "CURRENT ROW", "UNBOUNDED PRECEDING", etc. + } + + // If expr is a SQLNode, process it through the visitor + if (expr && typeof expr === 'object' && expr.type) { + return `${this.toString(expr)} ${scope}`; + } + + // If expr is a raw value (number, string, etc), use it directly + return `${expr} ${scope}`; + } + + visitWindowFrame(node: WindowFrameNode): string { + const { frameType, extent, exclude } = node; + if (!extent) return ''; + + // Handle ParamNode extent + if ((extent as any).type === 'PARAM') { + return `${frameType} ${this.toString(extent as ParamNode)}`; + } + + // Handle array extent [start, end] + if (Array.isArray(extent) && extent.length === 2) { + const [start, end] = extent; + const startExpr = this.formatFrameValue(start, 'PRECEDING'); + const endExpr = this.formatFrameValue(end, 'FOLLOWING'); + const excludeClause = exclude ? ` EXCLUDE ${exclude}` : ''; + return `${frameType} BETWEEN ${startExpr} AND ${endExpr}${excludeClause}`; + } + + return ''; + } + + private formatFrameValue(value: unknown, defaultScope: string): string { + // Handle WindowFrameExprNode + if (value && typeof value === 'object' && 'type' in value && value.type === 'WINDOW_EXTENT_EXPR') { + return this.toString(value as SQLNode); + } + + // Handle other SQLNode types + if (value && typeof value === 'object' && 'type' in value) { + return this.toString(value as SQLNode); + } + + // Handle raw values using the same logic as asFrameExpr + if (value != null && typeof value !== 'number') { + return `${value} ${defaultScope}`; + } + if (value === 0) { + return 'CURRENT ROW'; + } + if (!(value && Number.isFinite(value as number))) { + return `UNBOUNDED ${defaultScope}`; + } + return `${Math.abs(value as number)} ${defaultScope}`; + } + + visitWindowFunction(node: WindowFunctionNode): string { + const { name, args, ignoreNulls, order } = node; + const arg = [ + this.mapToString(args).join(', '), + order.length ? `ORDER BY ${this.mapToString(order).join(', ')}` : '', + ignoreNulls ? 'IGNORE NULLS' : '' + ].filter(x => x).join(' '); + return `${name}(${arg})`; + } + + visitWithClause(node: WithClauseNode): string { + const { name, query, materialized } = node; + const mat = materialized === true ? 'MATERIALIZED ' + : materialized === false ? 'NOT MATERIALIZED ' + : ''; + return `${quoteIdentifier(name)} AS ${mat}(${this.toString(query)})`; + } +} + +// Create a default DuckDB visitor instance for convenience +export const duckdbVisitor = new DuckDBVisitor(); diff --git a/packages/mosaic/sql/src/visit/index.ts b/packages/mosaic/sql/src/visit/index.ts new file mode 100644 index 000000000..bb0f51688 --- /dev/null +++ b/packages/mosaic/sql/src/visit/index.ts @@ -0,0 +1,6 @@ +export { SQLVisitor, BaseSQLVisitor } from './to-string-visitor.js'; +export { DuckDBVisitor, duckdbVisitor } from './duckdb-visitor.js'; +export { walk } from './walk.js'; + +// Re-export for convenience +export { duckdbVisitor as defaultVisitor } from './duckdb-visitor.js'; \ No newline at end of file diff --git a/packages/mosaic/sql/src/visit/to-string-visitor.ts b/packages/mosaic/sql/src/visit/to-string-visitor.ts new file mode 100644 index 000000000..e25d1b95d --- /dev/null +++ b/packages/mosaic/sql/src/visit/to-string-visitor.ts @@ -0,0 +1,204 @@ +import type { + SQLNode, + ExprNode, + AggregateNode, + BetweenOpNode, + NotBetweenOpNode, + BinaryOpNode, + CaseNode, + WhenNode, + CastNode, + CollateNode, + ColumnParamNode, + ColumnRefNode, + FragmentNode, + FromClauseNode, + FunctionNode, + InOpNode, + IntervalNode, + ListNode, + LiteralNode, + LogicalOpNode, + OrderByNode, + ParamNode, + DescribeQuery, + SelectQuery, + SetOperation, + SampleClauseNode, + SelectClauseNode, + ScalarSubqueryNode, + TableRefNode, + UnaryOpNode, + UnaryPostfixOpNode, + UnnestNode, + VerbatimNode, + WindowNode, + WindowClauseNode, + WindowDefNode, + WindowFunctionNode, + WindowFrameNode, + WindowFrameExprNode, + WithClauseNode +} from '../index.js'; +import { + AGGREGATE, + BETWEEN_OPERATOR, + BINARY_OPERATOR, + CASE, + CAST, + COLLATE, + COLUMN_PARAM, + COLUMN_REF, + DESCRIBE_QUERY, + EXPRESSION, + FRAGMENT, + FROM_CLAUSE, + FUNCTION, + IN_OPERATOR, + INTERVAL, + LIST, + LITERAL, + LOGICAL_OPERATOR, + NOT_BETWEEN_OPERATOR, + ORDER_BY, + PARAM, + SAMPLE_CLAUSE, + SCALAR_SUBQUERY, + SELECT_CLAUSE, + SELECT_QUERY, + SET_OPERATION, + TABLE_REF, + UNARY_OPERATOR, + UNARY_POSTFIX_OPERATOR, + UNNEST, + VERBATIM, + WHEN, + WINDOW, + WINDOW_CLAUSE, + WINDOW_DEF, + WINDOW_EXTENT_EXPR, + WINDOW_FRAME, + WINDOW_FUNCTION, + WITH_CLAUSE +} from '../constants.js'; + +/** + * Abstract base class for SQL visitors providing common functionality. + */ +export abstract class ToStringVisitor { + /** + * Convert a SQL AST node to a string using this visitor. + * @param node The SQL AST node to convert. + * @returns The SQL string representation. + */ + toString(node: SQLNode): string { + if (!node) { + throw new Error('Node is null or undefined'); + } + if (typeof node.type !== 'string') { + throw new Error(`Node type is not a string: ${typeof node.type}, value: ${node.type}`); + } + const method = this.getVisitMethod(node.type); + if (typeof method === 'function') { + // @ts-expect-error: fix me + return method.call(this, node); + } + throw new Error(`No visitor method for node type: '${node.type}'`); + } + + protected getVisitMethod(nodeType: string) { + switch (nodeType) { + case AGGREGATE: return this.visitAggregate; + case BETWEEN_OPERATOR: return this.visitBetween; + case BINARY_OPERATOR: return this.visitBinary; + case CASE: return this.visitCase; + case CAST: return this.visitCast; + case COLLATE: return this.visitCollate; + case COLUMN_PARAM: return this.visitColumnParam; + case COLUMN_REF: return this.visitColumnRef; + case DESCRIBE_QUERY: return this.visitDescribeQuery; + case EXPRESSION: return this.visitExpression; + case FRAGMENT: return this.visitFragment; + case FROM_CLAUSE: return this.visitFromClause; + case FUNCTION: return this.visitFunction; + case IN_OPERATOR: return this.visitIn; + case INTERVAL: return this.visitInterval; + case LIST: return this.visitList; + case LITERAL: return this.visitLiteral; + case LOGICAL_OPERATOR: return this.visitLogicalOperator; + case NOT_BETWEEN_OPERATOR: return this.visitNotBetween; + case ORDER_BY: return this.visitOrderBy; + case PARAM: return this.visitParam; + case SAMPLE_CLAUSE: return this.visitSampleClause; + case SCALAR_SUBQUERY: return this.visitScalarSubquery; + case SELECT_CLAUSE: return this.visitSelectClause; + case SELECT_QUERY: return this.visitSelectQuery; + case SET_OPERATION: return this.visitSetOperation; + case TABLE_REF: return this.visitTableRef; + case UNARY_OPERATOR: return this.visitUnary; + case UNARY_POSTFIX_OPERATOR: return this.visitUnaryPostfix; + case UNNEST: return this.visitUnnest; + case VERBATIM: return this.visitVerbatim; + case WHEN: return this.visitWhen; + case WINDOW: return this.visitWindow; + case WINDOW_CLAUSE: return this.visitWindowClause; + case WINDOW_DEF: return this.visitWindowDef; + case WINDOW_EXTENT_EXPR: return this.visitWindowExtentExpr; + case WINDOW_FRAME: return this.visitWindowFrame; + case WINDOW_FUNCTION: return this.visitWindowFunction; + case WITH_CLAUSE: return this.visitWithClause; + default: + throw new Error(`Unknown node type: '${nodeType}'`); + } + } + + /** + * Helper method to convert child nodes to strings. + * @param nodes Array of child nodes. + * @returns Array of SQL strings. + */ + protected mapToString(nodes: SQLNode[]): string[] { + return nodes.map(node => this.toString(node)); + } + + // Abstract methods that must be implemented by concrete visitors + abstract visitAggregate(node: AggregateNode): string; + abstract visitBetween(node: BetweenOpNode): string; + abstract visitBinary(node: BinaryOpNode): string; + abstract visitCase(node: CaseNode): string; + abstract visitCast(node: CastNode): string; + abstract visitCollate(node: CollateNode): string; + abstract visitColumnParam(node: ColumnParamNode): string; + abstract visitColumnRef(node: ColumnRefNode): string; + abstract visitDescribeQuery(node: DescribeQuery): string; + abstract visitExpression(node: ExprNode): string; + abstract visitFragment(node: FragmentNode): string; + abstract visitFromClause(node: FromClauseNode): string; + abstract visitFunction(node: FunctionNode): string; + abstract visitIn(node: InOpNode): string; + abstract visitInterval(node: IntervalNode): string; + abstract visitList(node: ListNode): string; + abstract visitLiteral(node: LiteralNode): string; + abstract visitLogicalOperator(node: LogicalOpNode): string; + abstract visitNotBetween(node: NotBetweenOpNode): string; + abstract visitOrderBy(node: OrderByNode): string; + abstract visitParam(node: ParamNode): string; + abstract visitSampleClause(node: SampleClauseNode): string; + abstract visitScalarSubquery(node: ScalarSubqueryNode): string; + abstract visitSelectClause(node: SelectClauseNode): string; + abstract visitSelectQuery(node: SelectQuery): string; + abstract visitSetOperation(node: SetOperation): string; + abstract visitTableRef(node: TableRefNode): string; + abstract visitUnary(node: UnaryOpNode): string; + abstract visitUnaryPostfix(node: UnaryPostfixOpNode): string; + abstract visitUnnest(node: UnnestNode): string; + abstract visitVerbatim(node: VerbatimNode): string; + abstract visitWhen(node: WhenNode): string; + abstract visitWindow(node: WindowNode): string; + abstract visitWindowClause(node: WindowClauseNode): string; + abstract visitWindowDef(node: WindowDefNode): string; + abstract visitWindowExtentExpr(node: WindowFrameExprNode): string; + abstract visitWindowFrame(node: WindowFrameNode): string; + abstract visitWindowFunction(node: WindowFunctionNode): string; + abstract visitWithClause(node: WithClauseNode): string; +} From 5dbcc7a8b13e69b7cce42efe52df0a47905c5697 Mon Sep 17 00:00:00 2001 From: Dominik Moritz Date: Wed, 17 Sep 2025 13:19:26 -0400 Subject: [PATCH 2/4] fix build --- packages/mosaic/sql/src/ast/node.ts | 8 ++++---- packages/mosaic/sql/src/index.ts | 1 - packages/mosaic/sql/src/visit/index.ts | 6 ------ 3 files changed, 4 insertions(+), 11 deletions(-) delete mode 100644 packages/mosaic/sql/src/visit/index.ts diff --git a/packages/mosaic/sql/src/ast/node.ts b/packages/mosaic/sql/src/ast/node.ts index 39dfbf640..460a700d4 100644 --- a/packages/mosaic/sql/src/ast/node.ts +++ b/packages/mosaic/sql/src/ast/node.ts @@ -1,4 +1,4 @@ -import type { BaseToStringVisitor } from '../visit/to-string-visitor.js'; +import type { ToStringVisitor } from '../visit/to-string-visitor.js'; /** * Check if a value is a SQL AST node. @@ -8,13 +8,13 @@ export function isNode(value: unknown): value is SQLNode { return value instanceof SQLNode; } -let _defaultVisitor: BaseToStringVisitor | null = null; +let _defaultVisitor: ToStringVisitor | null = null; /** * Set the default visitor for toString operations. * This is used when no visitor is explicitly provided. */ -export function setDefaultVisitor(visitor: BaseToStringVisitor) { +export function setDefaultVisitor(visitor: ToStringVisitor) { _defaultVisitor = visitor; } @@ -50,7 +50,7 @@ export class SQLNode { * @param visitor Optional SQL visitor to use for string generation. If not provided, uses the default visitor. * @returns The SQL string representation. */ - toString(visitor?: BaseToStringVisitor): string { + toString(visitor?: ToStringVisitor): string { const visitorToUse = visitor || _defaultVisitor; if (!visitorToUse) { throw new Error('No visitor provided and no default visitor set.'); diff --git a/packages/mosaic/sql/src/index.ts b/packages/mosaic/sql/src/index.ts index 2079c36b1..83aa3cb07 100644 --- a/packages/mosaic/sql/src/index.ts +++ b/packages/mosaic/sql/src/index.ts @@ -62,7 +62,6 @@ export { collectAggregates, collectColumns, collectParams, isAggregateExpression export { walk, type VisitorCallback, type VisitorResult } from './visit/walk.js'; export { ToStringVisitor } from './visit/to-string-visitor.js'; export { DuckDBVisitor, duckdbVisitor } from './visit/duckdb-visitor.js'; -export { defaultVisitor } from './visit/index.js'; export { createTable, createSchema } from './load/create.js'; export { loadExtension } from './load/extension.js'; diff --git a/packages/mosaic/sql/src/visit/index.ts b/packages/mosaic/sql/src/visit/index.ts deleted file mode 100644 index bb0f51688..000000000 --- a/packages/mosaic/sql/src/visit/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { SQLVisitor, BaseSQLVisitor } from './to-string-visitor.js'; -export { DuckDBVisitor, duckdbVisitor } from './duckdb-visitor.js'; -export { walk } from './walk.js'; - -// Re-export for convenience -export { duckdbVisitor as defaultVisitor } from './duckdb-visitor.js'; \ No newline at end of file From 483ec33298a68d2c80a53308eaf98cf06b6b0be3 Mon Sep 17 00:00:00 2001 From: Dominik Moritz Date: Wed, 17 Sep 2025 13:20:50 -0400 Subject: [PATCH 3/4] style: empty lines --- packages/mosaic/sql/src/ast/aggregate.ts | 2 -- packages/mosaic/sql/src/ast/between-op.ts | 4 ---- packages/mosaic/sql/src/ast/binary-op.ts | 2 -- packages/mosaic/sql/src/ast/case.ts | 4 ---- packages/mosaic/sql/src/ast/cast.ts | 2 -- packages/mosaic/sql/src/ast/collate.ts | 2 -- packages/mosaic/sql/src/ast/column-ref.ts | 2 -- packages/mosaic/sql/src/ast/fragment.ts | 2 -- packages/mosaic/sql/src/ast/function.ts | 2 -- packages/mosaic/sql/src/ast/in-op.ts | 2 -- packages/mosaic/sql/src/ast/interval.ts | 2 -- packages/mosaic/sql/src/ast/list.ts | 2 -- packages/mosaic/sql/src/ast/literal.ts | 2 -- packages/mosaic/sql/src/ast/logical-op.ts | 2 -- packages/mosaic/sql/src/ast/order-by.ts | 2 -- packages/mosaic/sql/src/ast/param.ts | 2 -- packages/mosaic/sql/src/ast/query.ts | 6 ------ packages/mosaic/sql/src/ast/sample.ts | 2 -- packages/mosaic/sql/src/ast/subquery.ts | 2 -- packages/mosaic/sql/src/ast/table-ref.ts | 2 -- packages/mosaic/sql/src/ast/unary-op.ts | 4 ---- packages/mosaic/sql/src/ast/unnest.ts | 2 -- packages/mosaic/sql/src/ast/verbatim.ts | 2 -- packages/mosaic/sql/src/ast/window-frame.ts | 2 -- packages/mosaic/sql/src/ast/window.ts | 8 -------- packages/mosaic/sql/src/ast/with.ts | 2 -- 26 files changed, 68 deletions(-) diff --git a/packages/mosaic/sql/src/ast/aggregate.ts b/packages/mosaic/sql/src/ast/aggregate.ts index bbe20c5fd..1aed1819f 100644 --- a/packages/mosaic/sql/src/ast/aggregate.ts +++ b/packages/mosaic/sql/src/ast/aggregate.ts @@ -104,8 +104,6 @@ export class AggregateNode extends ExprNode { frame(framedef: WindowFrameNode) { return this.window().frame(framedef); } - - } /** diff --git a/packages/mosaic/sql/src/ast/between-op.ts b/packages/mosaic/sql/src/ast/between-op.ts index 980d343ed..0981041bb 100644 --- a/packages/mosaic/sql/src/ast/between-op.ts +++ b/packages/mosaic/sql/src/ast/between-op.ts @@ -40,8 +40,6 @@ export class BetweenOpNode extends AbstractBetweenOpNode { constructor(expr: ExprNode, extent?: Extent) { super(BETWEEN_OPERATOR, expr, extent); } - - } export class NotBetweenOpNode extends AbstractBetweenOpNode { @@ -53,6 +51,4 @@ export class NotBetweenOpNode extends AbstractBetweenOpNode { constructor(expr: ExprNode, extent?: Extent) { super(NOT_BETWEEN_OPERATOR, expr, extent); } - - } diff --git a/packages/mosaic/sql/src/ast/binary-op.ts b/packages/mosaic/sql/src/ast/binary-op.ts index ecc77bf7f..fd0dbdc5b 100644 --- a/packages/mosaic/sql/src/ast/binary-op.ts +++ b/packages/mosaic/sql/src/ast/binary-op.ts @@ -21,6 +21,4 @@ export class BinaryOpNode extends ExprNode { this.left = left; this.right = right; } - - } diff --git a/packages/mosaic/sql/src/ast/case.ts b/packages/mosaic/sql/src/ast/case.ts index 7b481a00e..a325d1691 100644 --- a/packages/mosaic/sql/src/ast/case.ts +++ b/packages/mosaic/sql/src/ast/case.ts @@ -52,8 +52,6 @@ export class CaseNode extends ExprNode { else(expr: ExprValue) { return new CaseNode(this.expr, this._when, asNode(expr)); } - - } export class WhenNode extends SQLNode { @@ -72,6 +70,4 @@ export class WhenNode extends SQLNode { this.when = when; this.then = then; } - - } diff --git a/packages/mosaic/sql/src/ast/cast.ts b/packages/mosaic/sql/src/ast/cast.ts index 1ddfb1c88..b815f8c7c 100644 --- a/packages/mosaic/sql/src/ast/cast.ts +++ b/packages/mosaic/sql/src/ast/cast.ts @@ -17,6 +17,4 @@ export class CastNode extends ExprNode { this.expr = expr; this.cast = type; } - - } diff --git a/packages/mosaic/sql/src/ast/collate.ts b/packages/mosaic/sql/src/ast/collate.ts index aee52b823..5ddb0a396 100644 --- a/packages/mosaic/sql/src/ast/collate.ts +++ b/packages/mosaic/sql/src/ast/collate.ts @@ -17,6 +17,4 @@ export class CollateNode extends ExprNode { this.expr = expr; this.collation = collation; } - - } diff --git a/packages/mosaic/sql/src/ast/column-ref.ts b/packages/mosaic/sql/src/ast/column-ref.ts index e3d654b12..c7106d8e7 100644 --- a/packages/mosaic/sql/src/ast/column-ref.ts +++ b/packages/mosaic/sql/src/ast/column-ref.ts @@ -30,8 +30,6 @@ export class ColumnRefNode extends ExprNode { get column() { return ''; // subclasses to override } - - } export class ColumnNameRefNode extends ColumnRefNode { diff --git a/packages/mosaic/sql/src/ast/fragment.ts b/packages/mosaic/sql/src/ast/fragment.ts index 1fc30289c..62fd36e2a 100644 --- a/packages/mosaic/sql/src/ast/fragment.ts +++ b/packages/mosaic/sql/src/ast/fragment.ts @@ -18,6 +18,4 @@ export class FragmentNode extends ExprNode { super(FRAGMENT); this.spans = spans; } - - } diff --git a/packages/mosaic/sql/src/ast/function.ts b/packages/mosaic/sql/src/ast/function.ts index fdc751503..c97e8a97a 100644 --- a/packages/mosaic/sql/src/ast/function.ts +++ b/packages/mosaic/sql/src/ast/function.ts @@ -17,6 +17,4 @@ export class FunctionNode extends ExprNode { this.name = name; this.args = args; } - - } diff --git a/packages/mosaic/sql/src/ast/in-op.ts b/packages/mosaic/sql/src/ast/in-op.ts index deabf24c5..e2a697fc4 100644 --- a/packages/mosaic/sql/src/ast/in-op.ts +++ b/packages/mosaic/sql/src/ast/in-op.ts @@ -17,6 +17,4 @@ export class InOpNode extends ExprNode { this.expr = expr; this.values = values; } - - } diff --git a/packages/mosaic/sql/src/ast/interval.ts b/packages/mosaic/sql/src/ast/interval.ts index 1e8f9678e..69a30145e 100644 --- a/packages/mosaic/sql/src/ast/interval.ts +++ b/packages/mosaic/sql/src/ast/interval.ts @@ -17,6 +17,4 @@ export class IntervalNode extends ExprNode { this.name = name; this.steps = steps; } - - } diff --git a/packages/mosaic/sql/src/ast/list.ts b/packages/mosaic/sql/src/ast/list.ts index 91b2fc7dd..a4ea121b1 100644 --- a/packages/mosaic/sql/src/ast/list.ts +++ b/packages/mosaic/sql/src/ast/list.ts @@ -13,6 +13,4 @@ export class ListNode extends ExprNode { super(LIST); this.values = values; } - - } diff --git a/packages/mosaic/sql/src/ast/literal.ts b/packages/mosaic/sql/src/ast/literal.ts index e48b956fd..98d5e6435 100644 --- a/packages/mosaic/sql/src/ast/literal.ts +++ b/packages/mosaic/sql/src/ast/literal.ts @@ -13,8 +13,6 @@ export class LiteralNode extends ExprNode { super(LITERAL); this.value = value; } - - } export function literalToSQL(value: unknown) { diff --git a/packages/mosaic/sql/src/ast/logical-op.ts b/packages/mosaic/sql/src/ast/logical-op.ts index 79e2ba2f9..1a0a2fb05 100644 --- a/packages/mosaic/sql/src/ast/logical-op.ts +++ b/packages/mosaic/sql/src/ast/logical-op.ts @@ -17,8 +17,6 @@ export class LogicalOpNode extends ExprNode { this.op = op; this.clauses = clauses; } - - } export class AndNode extends LogicalOpNode { diff --git a/packages/mosaic/sql/src/ast/order-by.ts b/packages/mosaic/sql/src/ast/order-by.ts index 22f0566db..fa4eec003 100644 --- a/packages/mosaic/sql/src/ast/order-by.ts +++ b/packages/mosaic/sql/src/ast/order-by.ts @@ -21,6 +21,4 @@ export class OrderByNode extends ExprNode { this.desc = desc; this.nullsFirst = nullsFirst; } - - } diff --git a/packages/mosaic/sql/src/ast/param.ts b/packages/mosaic/sql/src/ast/param.ts index 621df8356..ccbcac25b 100644 --- a/packages/mosaic/sql/src/ast/param.ts +++ b/packages/mosaic/sql/src/ast/param.ts @@ -21,6 +21,4 @@ export class ParamNode extends ExprNode { get value() { return this.param.value; } - - } diff --git a/packages/mosaic/sql/src/ast/query.ts b/packages/mosaic/sql/src/ast/query.ts index 9a9dea70d..bede2577a 100644 --- a/packages/mosaic/sql/src/ast/query.ts +++ b/packages/mosaic/sql/src/ast/query.ts @@ -454,8 +454,6 @@ export class SelectQuery extends Query { this._qualify = this._qualify.concat(exprList(expr, asVerbatim)); return this; } - - } export class DescribeQuery extends SQLNode { @@ -477,8 +475,6 @@ export class DescribeQuery extends SQLNode { // @ts-expect-error creates describe query return new DescribeQuery(this.query.clone()); } - - } export class SetOperation extends Query { @@ -523,8 +519,6 @@ export class SetOperation extends Query { // @ts-expect-error creates set operation return Object.assign(new SetOperation(op, queries), rest); } - - } class WithClause { diff --git a/packages/mosaic/sql/src/ast/sample.ts b/packages/mosaic/sql/src/ast/sample.ts index 1d262909b..91fcb39c6 100644 --- a/packages/mosaic/sql/src/ast/sample.ts +++ b/packages/mosaic/sql/src/ast/sample.ts @@ -34,6 +34,4 @@ export class SampleClauseNode extends SQLNode { this.method = method; this.seed = seed; } - - } diff --git a/packages/mosaic/sql/src/ast/subquery.ts b/packages/mosaic/sql/src/ast/subquery.ts index 9d02dbd95..eb590fb43 100644 --- a/packages/mosaic/sql/src/ast/subquery.ts +++ b/packages/mosaic/sql/src/ast/subquery.ts @@ -14,6 +14,4 @@ export class ScalarSubqueryNode extends ExprNode { super(SCALAR_SUBQUERY); this.subquery = subquery; } - - } diff --git a/packages/mosaic/sql/src/ast/table-ref.ts b/packages/mosaic/sql/src/ast/table-ref.ts index 8437f1735..23833152e 100644 --- a/packages/mosaic/sql/src/ast/table-ref.ts +++ b/packages/mosaic/sql/src/ast/table-ref.ts @@ -28,6 +28,4 @@ export class TableRefNode extends ExprNode { get name() { return this.table[this.table.length - 1]; } - - } diff --git a/packages/mosaic/sql/src/ast/unary-op.ts b/packages/mosaic/sql/src/ast/unary-op.ts index 01b6c9959..c4dfd6e77 100644 --- a/packages/mosaic/sql/src/ast/unary-op.ts +++ b/packages/mosaic/sql/src/ast/unary-op.ts @@ -29,8 +29,6 @@ export class UnaryOpNode extends AbstractUnaryOpNode { constructor(op: string, expr: ExprNode) { super(UNARY_OPERATOR, op, expr); } - - } export class UnaryPostfixOpNode extends AbstractUnaryOpNode { @@ -42,6 +40,4 @@ export class UnaryPostfixOpNode extends AbstractUnaryOpNode { constructor(op: string, expr: ExprNode) { super(UNARY_POSTFIX_OPERATOR, op, expr); } - - } diff --git a/packages/mosaic/sql/src/ast/unnest.ts b/packages/mosaic/sql/src/ast/unnest.ts index 3b57bd14d..6003bb724 100644 --- a/packages/mosaic/sql/src/ast/unnest.ts +++ b/packages/mosaic/sql/src/ast/unnest.ts @@ -19,6 +19,4 @@ export class UnnestNode extends ExprNode { this.recursive = recursive; this.maxDepth = maxDepth; } - - } diff --git a/packages/mosaic/sql/src/ast/verbatim.ts b/packages/mosaic/sql/src/ast/verbatim.ts index 105ba7784..0a51f670f 100644 --- a/packages/mosaic/sql/src/ast/verbatim.ts +++ b/packages/mosaic/sql/src/ast/verbatim.ts @@ -17,6 +17,4 @@ export class VerbatimNode extends ExprNode { this.value = value; this.hint = hint; } - - } diff --git a/packages/mosaic/sql/src/ast/window-frame.ts b/packages/mosaic/sql/src/ast/window-frame.ts index 23d19dc31..a9c40fce0 100644 --- a/packages/mosaic/sql/src/ast/window-frame.ts +++ b/packages/mosaic/sql/src/ast/window-frame.ts @@ -63,6 +63,4 @@ export class WindowFrameExprNode extends SQLNode { this.scope = scope; this.expr = expr; } - - } diff --git a/packages/mosaic/sql/src/ast/window.ts b/packages/mosaic/sql/src/ast/window.ts index c89113024..fd335450f 100644 --- a/packages/mosaic/sql/src/ast/window.ts +++ b/packages/mosaic/sql/src/ast/window.ts @@ -21,8 +21,6 @@ export class WindowClauseNode extends SQLNode { this.name = name; this.def = def; } - - } export class WindowNode extends ExprNode { @@ -78,8 +76,6 @@ export class WindowNode extends ExprNode { frame(framedef: WindowFrameNode) { return new WindowNode(this.func, this.def.frame(framedef)); } - - } export class WindowFunctionNode extends ExprNode { @@ -112,8 +108,6 @@ export class WindowFunctionNode extends ExprNode { this.ignoreNulls = ignoreNulls; this.order = nodeList([argOrder]); } - - } export class WindowDefNode extends SQLNode { @@ -181,8 +175,6 @@ export class WindowDefNode extends SQLNode { frame(framedef: WindowFrameNode) { return deriveDef(this, { framedef }); } - - } interface DeriveDefOptions { diff --git a/packages/mosaic/sql/src/ast/with.ts b/packages/mosaic/sql/src/ast/with.ts index ec47f42e8..cc6790428 100644 --- a/packages/mosaic/sql/src/ast/with.ts +++ b/packages/mosaic/sql/src/ast/with.ts @@ -29,6 +29,4 @@ export class WithClauseNode extends SQLNode { this.query = query; this.materialized = materialized; } - - } From df34653f26fa20ff7bb164abfbaf61a43a3e9349 Mon Sep 17 00:00:00 2001 From: jheer Date: Thu, 18 Sep 2025 11:36:39 -0700 Subject: [PATCH 4/4] fix: Fix SQL code gen bugs. --- docs/public/specs/esm/gaia.js | 2 +- docs/public/specs/json/gaia.json | 2 +- docs/public/specs/yaml/gaia.yaml | 2 +- packages/mosaic/sql/src/ast/join.ts | 19 --- packages/mosaic/sql/src/ast/node.ts | 16 +-- packages/mosaic/sql/src/constants.ts | 1 + packages/mosaic/sql/src/index.ts | 4 +- packages/mosaic/sql/src/init.ts | 4 +- .../{duckdb-visitor.ts => codegen/duckdb.ts} | 134 +++++++++--------- .../{to-string-visitor.ts => codegen/sql.ts} | 18 ++- packages/vgplot/plot/src/plot-renderer.js | 4 +- packages/vgplot/plot/src/transforms/bin.js | 2 +- specs/esm/gaia.js | 2 +- specs/json/gaia.json | 2 +- specs/ts/gaia.ts | 2 +- specs/yaml/gaia.yaml | 2 +- 16 files changed, 102 insertions(+), 114 deletions(-) rename packages/mosaic/sql/src/visit/{duckdb-visitor.ts => codegen/duckdb.ts} (80%) rename packages/mosaic/sql/src/visit/{to-string-visitor.ts => codegen/sql.ts} (93%) diff --git a/docs/public/specs/esm/gaia.js b/docs/public/specs/esm/gaia.js index 68775e3e7..82f10bf40 100644 --- a/docs/public/specs/esm/gaia.js +++ b/docs/public/specs/esm/gaia.js @@ -14,7 +14,7 @@ WITH prep AS ( WHERE parallax BETWEEN -5 AND 20 AND phot_g_mean_mag IS NOT NULL AND bp_rp IS NOT NULL ) SELECT - (1.340264 * lambda * cos(t)) / (sqrt(3)/2 * (1.340264 + (-0.081106 * 3 * t2) + (t6 * (0.000893 * 7 + 0.003796 * 9 * t2)))) AS u, + (1.340264 * "lambda" * cos(t)) / (sqrt(3)/2 * (1.340264 + (-0.081106 * 3 * t2) + (t6 * (0.000893 * 7 + 0.003796 * 9 * t2)))) AS u, t * (1.340264 + (-0.081106 * t2) + (t6 * (0.000893 + 0.003796 * t2))) AS v, * EXCLUDE('t', 't2', 't6') FROM prep` diff --git a/docs/public/specs/json/gaia.json b/docs/public/specs/json/gaia.json index 3d8a241de..a80ab4586 100644 --- a/docs/public/specs/json/gaia.json +++ b/docs/public/specs/json/gaia.json @@ -4,7 +4,7 @@ "description": "A 5M row sample of the 1.8B element Gaia star catalog.\nA `raster` sky map reveals our Milky Way galaxy. Select high parallax stars in the histogram to reveal a\n[Hertzsprung-Russel diagram](https://en.wikipedia.org/wiki/Hertzsprung%E2%80%93Russell_diagram)\nin the plot of stellar color vs. magnitude on the right.\n\n_You may need to wait a few seconds for the dataset to load._\n" }, "data": { - "gaia": "-- compute u and v with natural earth projection\nWITH prep AS (\n SELECT\n radians((-l + 540) % 360 - 180) AS lambda,\n radians(b) AS phi,\n asin(sqrt(3)/2 * sin(phi)) AS t,\n t^2 AS t2,\n t2^3 AS t6,\n *\n FROM 'https://pub-1da360b43ceb401c809f68ca37c7f8a4.r2.dev/data/gaia-5m.parquet'\n WHERE parallax BETWEEN -5 AND 20 AND phot_g_mean_mag IS NOT NULL AND bp_rp IS NOT NULL\n)\nSELECT\n (1.340264 * lambda * cos(t)) / (sqrt(3)/2 * (1.340264 + (-0.081106 * 3 * t2) + (t6 * (0.000893 * 7 + 0.003796 * 9 * t2)))) AS u,\n t * (1.340264 + (-0.081106 * t2) + (t6 * (0.000893 + 0.003796 * t2))) AS v,\n * EXCLUDE('t', 't2', 't6')\nFROM prep\n" + "gaia": "-- compute u and v with natural earth projection\nWITH prep AS (\n SELECT\n radians((-l + 540) % 360 - 180) AS lambda,\n radians(b) AS phi,\n asin(sqrt(3)/2 * sin(phi)) AS t,\n t^2 AS t2,\n t2^3 AS t6,\n *\n FROM 'https://pub-1da360b43ceb401c809f68ca37c7f8a4.r2.dev/data/gaia-5m.parquet'\n WHERE parallax BETWEEN -5 AND 20 AND phot_g_mean_mag IS NOT NULL AND bp_rp IS NOT NULL\n)\nSELECT\n (1.340264 * \"lambda\" * cos(t)) / (sqrt(3)/2 * (1.340264 + (-0.081106 * 3 * t2) + (t6 * (0.000893 * 7 + 0.003796 * 9 * t2)))) AS u,\n t * (1.340264 + (-0.081106 * t2) + (t6 * (0.000893 + 0.003796 * t2))) AS v,\n * EXCLUDE('t', 't2', 't6')\nFROM prep\n" }, "params": { "brush": { diff --git a/docs/public/specs/yaml/gaia.yaml b/docs/public/specs/yaml/gaia.yaml index 362c8a488..cf56b15cc 100644 --- a/docs/public/specs/yaml/gaia.yaml +++ b/docs/public/specs/yaml/gaia.yaml @@ -22,7 +22,7 @@ data: WHERE parallax BETWEEN -5 AND 20 AND phot_g_mean_mag IS NOT NULL AND bp_rp IS NOT NULL ) SELECT - (1.340264 * lambda * cos(t)) / (sqrt(3)/2 * (1.340264 + (-0.081106 * 3 * t2) + (t6 * (0.000893 * 7 + 0.003796 * 9 * t2)))) AS u, + (1.340264 * "lambda" * cos(t)) / (sqrt(3)/2 * (1.340264 + (-0.081106 * 3 * t2) + (t6 * (0.000893 * 7 + 0.003796 * 9 * t2)))) AS u, t * (1.340264 + (-0.081106 * t2) + (t6 * (0.000893 + 0.003796 * t2))) AS v, * EXCLUDE('t', 't2', 't6') FROM prep diff --git a/packages/mosaic/sql/src/ast/join.ts b/packages/mosaic/sql/src/ast/join.ts index 21fda8047..8445200c7 100644 --- a/packages/mosaic/sql/src/ast/join.ts +++ b/packages/mosaic/sql/src/ast/join.ts @@ -63,23 +63,4 @@ export class JoinNode extends FromNode { this.using = using; this.sample = sample; } - - /** - * Generate a SQL query string for this node. - */ - toString() { - const { left, right, joinVariant, joinType, condition, using, sample } = this; - const variant = joinVariant === 'REGULAR' ? '' : `${joinVariant} `; - let type = ''; - let cond = ''; - - if (joinVariant !== 'CROSS') { - type = joinType !== 'INNER' ? `${joinType} ` : ''; - cond = condition ? ` ON ${condition}` - : using ? ` USING (${using?.join(', ')})` - : ''; - } - const samp = sample ? ` USING SAMPLE ${sample}` : ''; - return `${left} ${variant}${type}JOIN ${right}${cond}${samp}`; - } } diff --git a/packages/mosaic/sql/src/ast/node.ts b/packages/mosaic/sql/src/ast/node.ts index 460a700d4..fe8784d30 100644 --- a/packages/mosaic/sql/src/ast/node.ts +++ b/packages/mosaic/sql/src/ast/node.ts @@ -1,4 +1,4 @@ -import type { ToStringVisitor } from '../visit/to-string-visitor.js'; +import type { SQLCodeGenerator } from '../visit/codegen/sql.js'; /** * Check if a value is a SQL AST node. @@ -8,13 +8,13 @@ export function isNode(value: unknown): value is SQLNode { return value instanceof SQLNode; } -let _defaultVisitor: ToStringVisitor | null = null; +let _defaultVisitor: SQLCodeGenerator | undefined; /** * Set the default visitor for toString operations. * This is used when no visitor is explicitly provided. */ -export function setDefaultVisitor(visitor: ToStringVisitor) { +export function setDefaultVisitor(visitor: SQLCodeGenerator) { _defaultVisitor = visitor; } @@ -47,15 +47,15 @@ export class SQLNode { /** * Generate a SQL query string for this node using a specific dialect visitor. - * @param visitor Optional SQL visitor to use for string generation. If not provided, uses the default visitor. + * @param visitor Optional SQL visitor to use for string generation. + * If not provided, uses the default visitor. * @returns The SQL string representation. */ - toString(visitor?: ToStringVisitor): string { - const visitorToUse = visitor || _defaultVisitor; - if (!visitorToUse) { + toString(visitor: SQLCodeGenerator | undefined = _defaultVisitor): string { + if (!visitor) { throw new Error('No visitor provided and no default visitor set.'); } - return visitorToUse.toString(this); + return visitor.toString(this); } } diff --git a/packages/mosaic/sql/src/constants.ts b/packages/mosaic/sql/src/constants.ts index 8d09aef5a..c36c90ee9 100644 --- a/packages/mosaic/sql/src/constants.ts +++ b/packages/mosaic/sql/src/constants.ts @@ -27,6 +27,7 @@ export const WINDOW_FUNCTION = 'WINDOW_FUNCTION'; export const WINDOW_DEF = 'WINDOW_DEF'; export const WINDOW_FRAME = 'WINDOW_FRAME'; export const WINDOW_EXTENT_EXPR = 'WINDOW_EXTENT_EXPR'; +export const CUSTOM = 'CUSTOM'; export const EXPRESSION = 'EXPRESSION'; export const FRAGMENT = 'FRAGMENT'; diff --git a/packages/mosaic/sql/src/index.ts b/packages/mosaic/sql/src/index.ts index 83aa3cb07..c5aefc13e 100644 --- a/packages/mosaic/sql/src/index.ts +++ b/packages/mosaic/sql/src/index.ts @@ -60,8 +60,8 @@ export { deepClone } from './visit/clone.js'; export { rewrite } from './visit/rewrite.js'; export { collectAggregates, collectColumns, collectParams, isAggregateExpression } from './visit/visitors.js'; export { walk, type VisitorCallback, type VisitorResult } from './visit/walk.js'; -export { ToStringVisitor } from './visit/to-string-visitor.js'; -export { DuckDBVisitor, duckdbVisitor } from './visit/duckdb-visitor.js'; +export { SQLCodeGenerator as ToStringVisitor } from './visit/codegen/sql.js'; +export { DuckDBCodeGenerator, duckDBCodeGenerator } from './visit/codegen/duckdb.js'; export { createTable, createSchema } from './load/create.js'; export { loadExtension } from './load/extension.js'; diff --git a/packages/mosaic/sql/src/init.ts b/packages/mosaic/sql/src/init.ts index 3497dabfd..c75b597f5 100644 --- a/packages/mosaic/sql/src/init.ts +++ b/packages/mosaic/sql/src/init.ts @@ -1,5 +1,5 @@ import { setDefaultVisitor } from './ast/node.js'; -import { duckdbVisitor } from './visit/duckdb-visitor.js'; +import { duckDBCodeGenerator } from './visit/codegen/duckdb.js'; // Initialize the default visitor -setDefaultVisitor(duckdbVisitor); +setDefaultVisitor(duckDBCodeGenerator); diff --git a/packages/mosaic/sql/src/visit/duckdb-visitor.ts b/packages/mosaic/sql/src/visit/codegen/duckdb.ts similarity index 80% rename from packages/mosaic/sql/src/visit/duckdb-visitor.ts rename to packages/mosaic/sql/src/visit/codegen/duckdb.ts index dc254e6ca..049d5d4b2 100644 --- a/packages/mosaic/sql/src/visit/duckdb-visitor.ts +++ b/packages/mosaic/sql/src/visit/codegen/duckdb.ts @@ -1,4 +1,3 @@ -import { ToStringVisitor } from './to-string-visitor.js'; import { SQLNode, ExprNode, @@ -17,6 +16,7 @@ import { FunctionNode, InOpNode, IntervalNode, + JoinNode, ListNode, LiteralNode, LogicalOpNode, @@ -39,28 +39,30 @@ import { WindowFunctionNode, WindowFrameNode, WindowFrameExprNode, - WithClauseNode -} from '../index.js'; -import { quoteIdentifier } from '../util/string.js'; -import { literalToSQL } from '../ast/literal.js'; + WithClauseNode, + isNode +} from '../../index.js'; +import { quoteIdentifier } from '../../util/string.js'; +import { literalToSQL } from '../../ast/literal.js'; +import { SQLCodeGenerator } from './sql.js'; +import { CURRENT_ROW, FOLLOWING, PRECEDING, UNBOUNDED } from '../../ast/window-frame.js'; +import { WINDOW_EXTENT_EXPR } from '../../constants.js'; function betweenToString(node: BetweenOpNode | NotBetweenOpNode, op: string) { const { extent: r, expr } = node; return r ? `(${expr} ${op} ${r[0]} AND ${r[1]})` : ''; } - function isColumnRefFor(expr: unknown, name: string): expr is ColumnRefNode { return expr instanceof ColumnRefNode && expr.table == null && expr.column === name; } - /** * DuckDB SQL dialect visitor for converting AST nodes to DuckDB-compatible SQL. */ -export class DuckDBVisitor extends ToStringVisitor { +export class DuckDBCodeGenerator extends SQLCodeGenerator { visitAggregate(node: AggregateNode): string { const { name, args, isDistinct, filter, order } = node; const arg = [ @@ -103,9 +105,7 @@ export class DuckDBVisitor extends ToStringVisitor { } visitColumnParam(node: ColumnParamNode): string { - const { param, table } = node; - const tref = table ? `${this.toString(table)}.` : ''; - return `${tref}${this.toString(param)}`; + return this.visitColumnRef(node); } visitColumnRef(node: ColumnRefNode): string { @@ -138,6 +138,7 @@ export class DuckDBVisitor extends ToStringVisitor { const isQuery = expr.type === 'SELECT_QUERY' || expr.type === 'SET_OPERATION'; const isTableRef = expr.type === 'TABLE_REF'; const ref = isQuery ? `(${this.toString(expr)})` : `${this.toString(expr)}`; + // eslint-disable-next-line @typescript-eslint/no-explicit-any const from = alias && !(isTableRef && (expr as any).table?.join('.') === alias) ? `${ref} AS ${quoteIdentifier(alias)}` : `${ref}`; @@ -159,6 +160,22 @@ export class DuckDBVisitor extends ToStringVisitor { return `INTERVAL ${steps} ${name}`; } + visitJoinClause(node: JoinNode): string { + const { left, right, joinVariant, joinType, condition, using, sample } = node; + const variant = joinVariant === 'REGULAR' ? '' : `${joinVariant} `; + let type = ''; + let cond = ''; + + if (joinVariant !== 'CROSS') { + type = joinType !== 'INNER' ? `${joinType} ` : ''; + cond = condition ? ` ON ${condition}` + : using ? ` USING (${using?.join(', ')})` + : ''; + } + const samp = sample ? ` USING SAMPLE ${sample}` : ''; + return `${left} ${variant}${type}JOIN ${right}${cond}${samp}`; + } + visitList(node: ListNode): string { const { values } = node; return `[${this.mapToString(values).join(', ')}]`; @@ -291,12 +308,25 @@ export class DuckDBVisitor extends ToStringVisitor { } visitSetOperation(node: SetOperation): string { - const { queries, op, _orderby } = node; - let sql = this.mapToString(queries).join(` ${op} `); - if (_orderby.length) { - sql += ` ORDER BY ${this.mapToString(_orderby).join(', ')}`; - } - return sql; + const { op, queries, _with, _orderby, _limitPerc, _limit, _offset } = node; + const sql = []; + + // WITH + if (_with.length) sql.push(`WITH ${_with.join(', ')}`); + + // SUBQUERIES + sql.push(queries.join(` ${op} `)); + + // ORDER BY + if (_orderby.length) sql.push(`ORDER BY ${_orderby.join(', ')}`); + + // LIMIT + if (_limit) sql.push(`LIMIT ${_limit}${_limitPerc ? '%' : ''}`); + + // OFFSET + if (_offset) sql.push(`OFFSET ${_offset}`); + + return sql.join(' '); } visitTableRef(node: TableRefNode): string { @@ -362,62 +392,28 @@ export class DuckDBVisitor extends ToStringVisitor { visitWindowExtentExpr(node: WindowFrameExprNode): string { const { scope, expr } = node; - if (expr === null || expr === undefined) { - return scope; // e.g., "CURRENT ROW", "UNBOUNDED PRECEDING", etc. - } - - // If expr is a SQLNode, process it through the visitor - if (expr && typeof expr === 'object' && expr.type) { - return `${this.toString(expr)} ${scope}`; - } - - // If expr is a raw value (number, string, etc), use it directly - return `${expr} ${scope}`; + return scope === CURRENT_ROW + ? scope + : `${isNode(expr) ? this.toString(expr) : (expr ?? UNBOUNDED)} ${scope}`; } visitWindowFrame(node: WindowFrameNode): string { - const { frameType, extent, exclude } = node; - if (!extent) return ''; - - // Handle ParamNode extent - if ((extent as any).type === 'PARAM') { - return `${frameType} ${this.toString(extent as ParamNode)}`; - } - - // Handle array extent [start, end] - if (Array.isArray(extent) && extent.length === 2) { - const [start, end] = extent; - const startExpr = this.formatFrameValue(start, 'PRECEDING'); - const endExpr = this.formatFrameValue(end, 'FOLLOWING'); - const excludeClause = exclude ? ` EXCLUDE ${exclude}` : ''; - return `${frameType} BETWEEN ${startExpr} AND ${endExpr}${excludeClause}`; - } - - return ''; + const { frameType, exclude, extent } = node; + const [prev, next] = isNode(extent) + ? extent.value as [unknown, unknown] + : extent; + const a = this.formatFrameExpr(prev, PRECEDING); + const b = this.formatFrameExpr(next, FOLLOWING); + return `${frameType} BETWEEN ${a} AND ${b}${exclude ? ` ${exclude}` : ''}`; } - private formatFrameValue(value: unknown, defaultScope: string): string { - // Handle WindowFrameExprNode - if (value && typeof value === 'object' && 'type' in value && value.type === 'WINDOW_EXTENT_EXPR') { - return this.toString(value as SQLNode); - } - - // Handle other SQLNode types - if (value && typeof value === 'object' && 'type' in value) { - return this.toString(value as SQLNode); - } - - // Handle raw values using the same logic as asFrameExpr - if (value != null && typeof value !== 'number') { - return `${value} ${defaultScope}`; - } - if (value === 0) { - return 'CURRENT ROW'; - } - if (!(value && Number.isFinite(value as number))) { - return `UNBOUNDED ${defaultScope}`; - } - return `${Math.abs(value as number)} ${defaultScope}`; + private formatFrameExpr(value: unknown, scope: string) { + const x = isNode(value) ? this.toString(value) : value; + return isNode(value) && value.type === WINDOW_EXTENT_EXPR ? x + : x != null && typeof x !== 'number' ? `${x} ${scope}` + : x === 0 ? CURRENT_ROW + : !(x && Number.isFinite(x)) ? `${UNBOUNDED} ${scope}` + : `${Math.abs(x)} ${scope}`; } visitWindowFunction(node: WindowFunctionNode): string { @@ -440,4 +436,4 @@ export class DuckDBVisitor extends ToStringVisitor { } // Create a default DuckDB visitor instance for convenience -export const duckdbVisitor = new DuckDBVisitor(); +export const duckDBCodeGenerator = new DuckDBCodeGenerator(); diff --git a/packages/mosaic/sql/src/visit/to-string-visitor.ts b/packages/mosaic/sql/src/visit/codegen/sql.ts similarity index 93% rename from packages/mosaic/sql/src/visit/to-string-visitor.ts rename to packages/mosaic/sql/src/visit/codegen/sql.ts index e25d1b95d..666b6de35 100644 --- a/packages/mosaic/sql/src/visit/to-string-visitor.ts +++ b/packages/mosaic/sql/src/visit/codegen/sql.ts @@ -16,6 +16,7 @@ import type { FunctionNode, InOpNode, IntervalNode, + JoinNode, ListNode, LiteralNode, LogicalOpNode, @@ -39,7 +40,7 @@ import type { WindowFrameNode, WindowFrameExprNode, WithClauseNode -} from '../index.js'; +} from '../../index.js'; import { AGGREGATE, BETWEEN_OPERATOR, @@ -56,6 +57,7 @@ import { FUNCTION, IN_OPERATOR, INTERVAL, + JOIN_CLAUSE, LIST, LITERAL, LOGICAL_OPERATOR, @@ -80,12 +82,12 @@ import { WINDOW_FRAME, WINDOW_FUNCTION, WITH_CLAUSE -} from '../constants.js'; +} from '../../constants.js'; /** - * Abstract base class for SQL visitors providing common functionality. + * Abstract base class for SQL code generation visitors. */ -export abstract class ToStringVisitor { +export abstract class SQLCodeGenerator { /** * Convert a SQL AST node to a string using this visitor. * @param node The SQL AST node to convert. @@ -98,9 +100,13 @@ export abstract class ToStringVisitor { if (typeof node.type !== 'string') { throw new Error(`Node type is not a string: ${typeof node.type}, value: ${node.type}`); } + if (node.type === 'CUSTOM') { + // custom node types may bypass visitor + return node.toString(); + } const method = this.getVisitMethod(node.type); if (typeof method === 'function') { - // @ts-expect-error: fix me + // @ts-expect-error: dispatch based on node type return method.call(this, node); } throw new Error(`No visitor method for node type: '${node.type}'`); @@ -123,6 +129,7 @@ export abstract class ToStringVisitor { case FUNCTION: return this.visitFunction; case IN_OPERATOR: return this.visitIn; case INTERVAL: return this.visitInterval; + case JOIN_CLAUSE: return this.visitJoinClause; case LIST: return this.visitList; case LITERAL: return this.visitLiteral; case LOGICAL_OPERATOR: return this.visitLogicalOperator; @@ -177,6 +184,7 @@ export abstract class ToStringVisitor { abstract visitFunction(node: FunctionNode): string; abstract visitIn(node: InOpNode): string; abstract visitInterval(node: IntervalNode): string; + abstract visitJoinClause(node: JoinNode): string; abstract visitList(node: ListNode): string; abstract visitLiteral(node: LiteralNode): string; abstract visitLogicalOperator(node: LogicalOpNode): string; diff --git a/packages/vgplot/plot/src/plot-renderer.js b/packages/vgplot/plot/src/plot-renderer.js index 054c3de7d..825ea13fb 100644 --- a/packages/vgplot/plot/src/plot-renderer.js +++ b/packages/vgplot/plot/src/plot-renderer.js @@ -123,7 +123,9 @@ function inferLabel(key, spec, marks) { function fieldLabel(field) { if (!field) return undefined; switch (field.type) { - case 'COLUMN_REF': return field.column; + case 'COLUMN_REF': + case 'CUSTOM': + return field.column; case 'CAST': return fieldLabel(field.expr); case 'FUNCTION': if (field.name === 'make_date') return 'date'; diff --git a/packages/vgplot/plot/src/transforms/bin.js b/packages/vgplot/plot/src/transforms/bin.js index 5a9ef96ad..8f03b2a1a 100644 --- a/packages/vgplot/plot/src/transforms/bin.js +++ b/packages/vgplot/plot/src/transforms/bin.js @@ -38,7 +38,7 @@ function hasTimeScale(mark, channel) { class BinTransformNode extends ExprNode { constructor(column, mark, channel, options) { - super('COLUMN_REF'); + super('CUSTOM'); this.column = column; this.mark = mark; this.channel = channel; diff --git a/specs/esm/gaia.js b/specs/esm/gaia.js index 68775e3e7..82f10bf40 100644 --- a/specs/esm/gaia.js +++ b/specs/esm/gaia.js @@ -14,7 +14,7 @@ WITH prep AS ( WHERE parallax BETWEEN -5 AND 20 AND phot_g_mean_mag IS NOT NULL AND bp_rp IS NOT NULL ) SELECT - (1.340264 * lambda * cos(t)) / (sqrt(3)/2 * (1.340264 + (-0.081106 * 3 * t2) + (t6 * (0.000893 * 7 + 0.003796 * 9 * t2)))) AS u, + (1.340264 * "lambda" * cos(t)) / (sqrt(3)/2 * (1.340264 + (-0.081106 * 3 * t2) + (t6 * (0.000893 * 7 + 0.003796 * 9 * t2)))) AS u, t * (1.340264 + (-0.081106 * t2) + (t6 * (0.000893 + 0.003796 * t2))) AS v, * EXCLUDE('t', 't2', 't6') FROM prep` diff --git a/specs/json/gaia.json b/specs/json/gaia.json index 1eef63b77..6a7b67c30 100644 --- a/specs/json/gaia.json +++ b/specs/json/gaia.json @@ -6,7 +6,7 @@ "data": { "gaia": { "type": "table", - "query": "-- compute u and v with natural earth projection\nWITH prep AS (\n SELECT\n radians((-l + 540) % 360 - 180) AS lambda,\n radians(b) AS phi,\n asin(sqrt(3)/2 * sin(phi)) AS t,\n t^2 AS t2,\n t2^3 AS t6,\n *\n FROM 'https://pub-1da360b43ceb401c809f68ca37c7f8a4.r2.dev/data/gaia-5m.parquet'\n WHERE parallax BETWEEN -5 AND 20 AND phot_g_mean_mag IS NOT NULL AND bp_rp IS NOT NULL\n)\nSELECT\n (1.340264 * lambda * cos(t)) / (sqrt(3)/2 * (1.340264 + (-0.081106 * 3 * t2) + (t6 * (0.000893 * 7 + 0.003796 * 9 * t2)))) AS u,\n t * (1.340264 + (-0.081106 * t2) + (t6 * (0.000893 + 0.003796 * t2))) AS v,\n * EXCLUDE('t', 't2', 't6')\nFROM prep" + "query": "-- compute u and v with natural earth projection\nWITH prep AS (\n SELECT\n radians((-l + 540) % 360 - 180) AS lambda,\n radians(b) AS phi,\n asin(sqrt(3)/2 * sin(phi)) AS t,\n t^2 AS t2,\n t2^3 AS t6,\n *\n FROM 'https://pub-1da360b43ceb401c809f68ca37c7f8a4.r2.dev/data/gaia-5m.parquet'\n WHERE parallax BETWEEN -5 AND 20 AND phot_g_mean_mag IS NOT NULL AND bp_rp IS NOT NULL\n)\nSELECT\n (1.340264 * \"lambda\" * cos(t)) / (sqrt(3)/2 * (1.340264 + (-0.081106 * 3 * t2) + (t6 * (0.000893 * 7 + 0.003796 * 9 * t2)))) AS u,\n t * (1.340264 + (-0.081106 * t2) + (t6 * (0.000893 + 0.003796 * t2))) AS v,\n * EXCLUDE('t', 't2', 't6')\nFROM prep" } }, "params": { diff --git a/specs/ts/gaia.ts b/specs/ts/gaia.ts index 63c21e7ed..f4729512a 100644 --- a/specs/ts/gaia.ts +++ b/specs/ts/gaia.ts @@ -6,7 +6,7 @@ export const spec : Spec = { "description": "A 5M row sample of the 1.8B element Gaia star catalog.\nA `raster` sky map reveals our Milky Way galaxy. Select high parallax stars in the histogram to reveal a\n[Hertzsprung-Russel diagram](https://en.wikipedia.org/wiki/Hertzsprung%E2%80%93Russell_diagram)\nin the plot of stellar color vs. magnitude on the right.\n\n_You may need to wait a few seconds for the dataset to load._\n" }, "data": { - "gaia": "-- compute u and v with natural earth projection\nWITH prep AS (\n SELECT\n radians((-l + 540) % 360 - 180) AS lambda,\n radians(b) AS phi,\n asin(sqrt(3)/2 * sin(phi)) AS t,\n t^2 AS t2,\n t2^3 AS t6,\n *\n FROM 'https://pub-1da360b43ceb401c809f68ca37c7f8a4.r2.dev/data/gaia-5m.parquet'\n WHERE parallax BETWEEN -5 AND 20 AND phot_g_mean_mag IS NOT NULL AND bp_rp IS NOT NULL\n)\nSELECT\n (1.340264 * lambda * cos(t)) / (sqrt(3)/2 * (1.340264 + (-0.081106 * 3 * t2) + (t6 * (0.000893 * 7 + 0.003796 * 9 * t2)))) AS u,\n t * (1.340264 + (-0.081106 * t2) + (t6 * (0.000893 + 0.003796 * t2))) AS v,\n * EXCLUDE('t', 't2', 't6')\nFROM prep\n" + "gaia": "-- compute u and v with natural earth projection\nWITH prep AS (\n SELECT\n radians((-l + 540) % 360 - 180) AS lambda,\n radians(b) AS phi,\n asin(sqrt(3)/2 * sin(phi)) AS t,\n t^2 AS t2,\n t2^3 AS t6,\n *\n FROM 'https://pub-1da360b43ceb401c809f68ca37c7f8a4.r2.dev/data/gaia-5m.parquet'\n WHERE parallax BETWEEN -5 AND 20 AND phot_g_mean_mag IS NOT NULL AND bp_rp IS NOT NULL\n)\nSELECT\n (1.340264 * \"lambda\" * cos(t)) / (sqrt(3)/2 * (1.340264 + (-0.081106 * 3 * t2) + (t6 * (0.000893 * 7 + 0.003796 * 9 * t2)))) AS u,\n t * (1.340264 + (-0.081106 * t2) + (t6 * (0.000893 + 0.003796 * t2))) AS v,\n * EXCLUDE('t', 't2', 't6')\nFROM prep\n" }, "params": { "brush": { diff --git a/specs/yaml/gaia.yaml b/specs/yaml/gaia.yaml index 362c8a488..cf56b15cc 100644 --- a/specs/yaml/gaia.yaml +++ b/specs/yaml/gaia.yaml @@ -22,7 +22,7 @@ data: WHERE parallax BETWEEN -5 AND 20 AND phot_g_mean_mag IS NOT NULL AND bp_rp IS NOT NULL ) SELECT - (1.340264 * lambda * cos(t)) / (sqrt(3)/2 * (1.340264 + (-0.081106 * 3 * t2) + (t6 * (0.000893 * 7 + 0.003796 * 9 * t2)))) AS u, + (1.340264 * "lambda" * cos(t)) / (sqrt(3)/2 * (1.340264 + (-0.081106 * 3 * t2) + (t6 * (0.000893 * 7 + 0.003796 * 9 * t2)))) AS u, t * (1.340264 + (-0.081106 * t2) + (t6 * (0.000893 + 0.003796 * t2))) AS v, * EXCLUDE('t', 't2', 't6') FROM prep