diff --git a/packages/mosaic/sql/src/ast/join.ts b/packages/mosaic/sql/src/ast/join.ts index 8445200c7..760dac9d1 100644 --- a/packages/mosaic/sql/src/ast/join.ts +++ b/packages/mosaic/sql/src/ast/join.ts @@ -3,7 +3,6 @@ import { JOIN_CLAUSE } from '../constants.js'; import { ColumnRefNode } from './column-ref.js'; import { FromNode } from './from.js'; import { ExprNode } from './node.js'; -import { TableRefNode } from './table-ref.js'; /** The join variant. Determines what kind of join is performed. */ export type JoinVariant = 'REGULAR' | 'CROSS' | 'NATURAL' | 'POSITIONAL' | 'ASOF'; @@ -13,9 +12,9 @@ export type JoinType = 'INNER' | 'LEFT' | 'RIGHT' | 'FULL' | 'SEMI' | 'ANTI'; export class JoinNode extends FromNode { /** The left table to join. */ - readonly left: FromNode | TableRefNode; + readonly left: FromNode; /** The right table to join. */ - readonly right: FromNode | TableRefNode; + readonly right: FromNode; /** The join variant (REGULAR, CROSS, NATURAL, POSITIONAL, ASOF). */ readonly joinVariant: JoinVariant; /** The join type (INNER, LEFT, RIGHT, FULL, SEMI, ANTI). */ @@ -46,8 +45,8 @@ export class JoinNode extends FromNode { * @param sample A table sample to apply to join output. */ constructor( - left: FromNode | TableRefNode, - right: FromNode | TableRefNode, + left: FromNode, + right: FromNode, variant: JoinVariant = 'NATURAL', type: JoinType = 'INNER', condition?: ExprNode, diff --git a/packages/mosaic/sql/src/functions/join.ts b/packages/mosaic/sql/src/functions/join.ts index c86b358cb..58d0c717b 100644 --- a/packages/mosaic/sql/src/functions/join.ts +++ b/packages/mosaic/sql/src/functions/join.ts @@ -2,7 +2,7 @@ import { ColumnRefNode } from '../ast/column-ref.js'; import { FromNode } from '../ast/from.js'; import { JoinNode, type JoinType, type JoinVariant } from '../ast/join.js'; import { ExprNode } from '../ast/node.js'; -import { asNode, asTableRef } from '../util/ast.js'; +import { asFrom, asNode } from '../util/ast.js'; type TableArg = string | string[] | FromNode; @@ -23,9 +23,6 @@ interface JoinOptions { using?: (string | ColumnRefNode)[]; } -function tableRef(x: TableArg) { - return x instanceof FromNode ? x : asTableRef(x)!; -} function makeJoin( left: TableArg, @@ -37,8 +34,8 @@ function makeJoin( throw new Error('Only one join condition (on or using) can be applied.'); } return new JoinNode( - tableRef(left), - tableRef(right), + asFrom(left), + asFrom(right), variant, options.type, options.on, diff --git a/packages/mosaic/sql/src/index.ts b/packages/mosaic/sql/src/index.ts index be97b5b7f..a86aad36d 100644 --- a/packages/mosaic/sql/src/index.ts +++ b/packages/mosaic/sql/src/index.ts @@ -48,7 +48,7 @@ export { lineDensity } from './transforms/line-density.js'; export { m4 } from './transforms/m4.js'; export { scaleTransform, type Scale, type ScaleDomain, type ScaleOptions, type ScaleTransform, type ScaleType } from './transforms/scales.js'; -export { asLiteral, asNode, asTableRef, asVerbatim, parseColumnRef, parseTableRef, over } from './util/ast.js'; +export { asFrom, asLiteral, asNode, asTableRef, asVerbatim, parseColumnRef, parseTableRef, over } from './util/ast.js'; export { isParamLike } from './util/type-check.js'; export { binSpec, binStep, type BinOptions } from './transforms/util/bin-step.js'; diff --git a/packages/mosaic/sql/src/transforms/filter-query.ts b/packages/mosaic/sql/src/transforms/filter-query.ts index e8a0f955c..474b70f94 100644 --- a/packages/mosaic/sql/src/transforms/filter-query.ts +++ b/packages/mosaic/sql/src/transforms/filter-query.ts @@ -2,7 +2,6 @@ import { SCALAR_SUBQUERY } from '../constants.js'; import type { FilterExpr } from '../types.js'; import { ColumnRefNode } from '../ast/column-ref.js'; import { FromClauseNode } from '../ast/from.js'; -import { JoinNode } from '../ast/join.js'; import { Query } from '../ast/query.js'; import { isTableRef, type TableRefNode } from '../ast/table-ref.js'; import { asTableRef } from '../util/ast.js'; @@ -63,16 +62,9 @@ export function filterPushdown( } // @ts-expect-error set read-only property node.table = [filteredName]; - if (parent instanceof FromClauseNode) { - if (!parent.alias) { - // @ts-expect-error set read-only property - parent.alias = visibleName; - } - } else if (parent instanceof JoinNode) { - // a bare join operand can not carry an alias, so wrap it in one - const side = parent.left === node ? 'left' : 'right'; + if (parent instanceof FromClauseNode && !parent.alias) { // @ts-expect-error set read-only property - parent[side] = new FromClauseNode(node, visibleName); + parent.alias = visibleName; } }); diff --git a/packages/mosaic/sql/src/util/ast.ts b/packages/mosaic/sql/src/util/ast.ts index 38d3150c6..61e93c692 100644 --- a/packages/mosaic/sql/src/util/ast.ts +++ b/packages/mosaic/sql/src/util/ast.ts @@ -1,5 +1,6 @@ import type { ColumnRefNode } from '../ast/column-ref.js'; import type { TableRefNode } from '../ast/table-ref.js'; +import { FromClauseNode, FromNode } from '../ast/from.js'; import { ExprNode, type SQLNode } from '../ast/node.js'; import { ParamNode } from '../ast/param.js'; import { WindowDefNode } from '../ast/window.js'; @@ -71,6 +72,18 @@ function getTableRef(value?: string | string[] | T): TableRefNode | T | undef : value; } +/** + * Interpret a value as a FROM clause AST node. Table names and references + * are wrapped in a clause aliased by the table name, as in `Query.from`. + * Existing FROM nodes are left as-is. + * @param value The value to interpret as a FROM clause AST node. + */ +export function asFrom(value: string | string[] | TableRefNode | FromNode): FromNode { + if (value instanceof FromNode) return value; + const ref = asTableRef(value)!; + return new FromClauseNode(ref, ref.name); +} + /** * Parse a string as a column reference, potentially with * dot ('.') delimited table, schema, and database references. diff --git a/packages/mosaic/sql/src/visit/codegen/duckdb.ts b/packages/mosaic/sql/src/visit/codegen/duckdb.ts index 2a3d5467c..017502fd8 100644 --- a/packages/mosaic/sql/src/visit/codegen/duckdb.ts +++ b/packages/mosaic/sql/src/visit/codegen/duckdb.ts @@ -151,7 +151,7 @@ export class DuckDBCodeGenerator extends SQLCodeGenerator { const ref = isQuery(expr) ? `(${this.toString(expr)})` : `${this.toString(expr)}`; let from: string; - if (alias && (columnNames?.length || !(isTableRef(expr) && expr.table?.join('.') === alias))) { + if (alias && (columnNames?.length || !(isTableRef(expr) && expr.name === alias))) { const names = columnNames?.length ? `(${columnNames.map(v => quoteIdentifier(v)).join(', ')})` : ''; diff --git a/packages/mosaic/sql/test/join.test.ts b/packages/mosaic/sql/test/join.test.ts index 22a9d5959..a9c977041 100644 --- a/packages/mosaic/sql/test/join.test.ts +++ b/packages/mosaic/sql/test/join.test.ts @@ -1,6 +1,6 @@ import { expect, describe, it } from 'vitest'; import { asof_join, column, cross_join, eq, from, join, JoinNode, positional_join, walk } from '../src/index.js'; -import { JOIN_CLAUSE, TABLE_REF } from '../src/constants.js'; +import { FROM_CLAUSE, JOIN_CLAUSE, TABLE_REF } from '../src/constants.js'; import { validateQuery } from './util/validate.js'; /** Validate a join clause fragment by wrapping it in a SELECT. */ @@ -40,6 +40,9 @@ describe('Join functions', () => { await validateJoin(join('t1', 't2', { type: 'SEMI', using }), '"t1" SEMI JOIN "t2" USING ("num1")'); await validateJoin(join('t1', 't2', { type: 'ANTI', using }), '"t1" ANTI JOIN "t2" USING ("num1")'); + // redundant aliases on schema-qualified operands are not serialized + expect(String(join(['s', 't1'], 't2'))).toBe('"s"."t1" NATURAL JOIN "t2"'); + // handles from clauses const X = from('t1').as('X'); const Y = from('t2').as('Y'); @@ -76,7 +79,7 @@ describe('Join functions', () => { expect(() => walk( join('t1', 't2'), (x) => { - if (x.type !== JOIN_CLAUSE && x.type !== TABLE_REF) { + if (x.type !== JOIN_CLAUSE && x.type !== FROM_CLAUSE && x.type !== TABLE_REF) { throw new Error('Unexpected node type.'); } }