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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions packages/mosaic/sql/src/ast/join.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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). */
Expand Down Expand Up @@ -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,
Expand Down
9 changes: 3 additions & 6 deletions packages/mosaic/sql/src/functions/join.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -23,9 +23,6 @@ interface JoinOptions {
using?: (string | ColumnRefNode)[];
}

function tableRef(x: TableArg) {
return x instanceof FromNode ? x : asTableRef(x)!;
}

function makeJoin(
left: TableArg,
Expand All @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/mosaic/sql/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
12 changes: 2 additions & 10 deletions packages/mosaic/sql/src/transforms/filter-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
}
});

Expand Down
13 changes: 13 additions & 0 deletions packages/mosaic/sql/src/util/ast.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -71,6 +72,18 @@ function getTableRef<T>(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.
Expand Down
2 changes: 1 addition & 1 deletion packages/mosaic/sql/src/visit/codegen/duckdb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(', ')})`
: '';
Expand Down
7 changes: 5 additions & 2 deletions packages/mosaic/sql/test/join.test.ts
Original file line number Diff line number Diff line change
@@ -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. */
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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.');
}
}
Expand Down