rename tslq to trql

This commit is contained in:
mpcgird
2025-12-23 17:20:06 +02:00
parent 97f29e6f64
commit 37a73b07eb
64 changed files with 2602 additions and 2602 deletions
@@ -13,12 +13,12 @@ import { cn } from "~/utils/cn";
import { Button } from "../primitives/Buttons";
import { getEditorSetup } from "./codeMirrorSetup";
import { darkTheme } from "./codeMirrorTheme";
import { createTSQLCompletion } from "./tsql/tsqlCompletion";
import { createTSQLLinter } from "./tsql/tsqlLinter";
import type { TableSchema } from "@internal/tsql";
import { createTRQLCompletion } from "~/components/code/trql/trqlCompletion";
import { createTRQLLinter } from "~/components/code/trql/trqlLinter";
import type { TableSchema } from "@internal/trql";
import { format as formatSQL } from "sql-formatter";
export interface TSQLEditorProps extends Omit<ReactCodeMirrorProps, "onBlur"> {
export interface TRQLEditorProps extends Omit<ReactCodeMirrorProps, "onBlur"> {
/** Initial value for the editor */
defaultValue?: string;
/** Whether the editor is read-only */
@@ -47,9 +47,9 @@ export interface TSQLEditorProps extends Omit<ReactCodeMirrorProps, "onBlur"> {
minHeight?: string;
}
type TSQLEditorDefaultProps = Partial<TSQLEditorProps>;
type TRQLEditorDefaultProps = Partial<TRQLEditorProps>;
const defaultProps: TSQLEditorDefaultProps = {
const defaultProps: TRQLEditorDefaultProps = {
readOnly: false,
basicSetup: false,
linterEnabled: true,
@@ -59,7 +59,7 @@ const defaultProps: TSQLEditorDefaultProps = {
schema: [],
};
export function TSQLEditor(opts: TSQLEditorProps) {
export function TRQLEditor(opts: TRQLEditorProps) {
const {
defaultValue = "",
readOnly = false,
@@ -94,22 +94,22 @@ export function TSQLEditor(opts: TSQLEditorProps) {
})
);
// Add custom TSQL completion
// Add custom TRQL completion
if (schema && schema.length > 0) {
exts.push(
autocompletion({
override: [createTSQLCompletion(schema)],
override: [createTRQLCompletion(schema)],
activateOnTyping: true,
maxRenderedOptions: 50,
})
);
}
// Add TSQL linter
// Add TRQL linter
if (linterEnabled) {
exts.push(lintGutter());
exts.push(
linter(createTSQLLinter({ schema }), {
linter(createTRQLLinter({ schema }), {
delay: 300, // Debounce linting for better performance
})
);
@@ -261,7 +261,7 @@ function isRightAlignedColumn(column: OutputColumnMetadata): boolean {
return isNumericType(type);
}
export function TSQLResultsTable({
export function TRQLResultsTable({
rows,
columns,
prettyFormatting = true,
@@ -0,0 +1,6 @@
// TRQL CodeMirror support
// Provides syntax highlighting, autocompletion, and linting for TRQL queries
export { createTRQLCompletion } from "./trqlCompletion";
export { createTRQLLinter, isValidTRQLQuery, getTRQLError, type TRQLLinterConfig } from "./trqlLinter";
@@ -1,6 +1,6 @@
import { describe, it, expect } from "vitest";
import { createTSQLCompletion } from "./tsqlCompletion";
import type { TableSchema, ColumnSchema } from "@internal/tsql";
import { createTRQLCompletion } from "./trqlCompletion";
import type { TableSchema, ColumnSchema } from "@internal/trql";
// Helper to create a mock completion context
function createMockContext(doc: string, pos: number, explicit = false) {
@@ -69,8 +69,8 @@ const testSchema: TableSchema[] = [
},
];
describe("createTSQLCompletion", () => {
const completionSource = createTSQLCompletion(testSchema);
describe("createTRQLCompletion", () => {
const completionSource = createTRQLCompletion(testSchema);
it("should return null for empty input without explicit trigger", () => {
const context = createMockContext("", 0, false);
@@ -1,9 +1,9 @@
import type { CompletionContext, CompletionResult, Completion } from "@codemirror/autocomplete";
import type { TableSchema, ColumnSchema } from "@internal/tsql";
import type { TableSchema, ColumnSchema } from "@internal/trql";
import {
TSQL_CLICKHOUSE_FUNCTIONS,
TSQL_AGGREGATIONS,
} from "@internal/tsql";
TRQL_CLICKHOUSE_FUNCTIONS,
TRQL_AGGREGATIONS,
} from "@internal/trql";
/**
* SQL keywords for autocomplete
@@ -77,13 +77,13 @@ function createKeywordCompletions(): Completion[] {
}
/**
* Create function completions from TSQL function definitions
* Create function completions from TRQL function definitions
*/
function createFunctionCompletions(): Completion[] {
const functions: Completion[] = [];
// Add regular functions
for (const [name, meta] of Object.entries(TSQL_CLICKHOUSE_FUNCTIONS)) {
for (const [name, meta] of Object.entries(TRQL_CLICKHOUSE_FUNCTIONS)) {
// Skip internal functions starting with _
if (name.startsWith("_")) continue;
@@ -99,7 +99,7 @@ function createFunctionCompletions(): Completion[] {
}
// Add aggregate functions with slightly higher boost
for (const [name, meta] of Object.entries(TSQL_AGGREGATIONS)) {
for (const [name, meta] of Object.entries(TRQL_AGGREGATIONS)) {
if (name.startsWith("_")) continue;
const argsHint =
@@ -348,12 +348,12 @@ function createEnumValueCompletions(columnSchema: ColumnSchema): Completion[] {
}
/**
* Create a TSQL-aware autocompletion source
* Create a TRQL-aware autocompletion source
*
* @param schema - Array of table schemas to use for completions
* @returns A CodeMirror completion source function
*/
export function createTSQLCompletion(
export function createTRQLCompletion(
schema: TableSchema[]
): (context: CompletionContext) => CompletionResult | null {
// Pre-compute static completions
@@ -1,77 +1,77 @@
import { describe, it, expect } from "vitest";
import { isValidTSQLQuery, getTSQLError } from "./tsqlLinter";
import { isValidTRQLQuery, getTRQLError } from "./trqlLinter";
describe("tsqlLinter", () => {
describe("isValidTSQLQuery", () => {
describe("trqlLinter", () => {
describe("isValidTRQLQuery", () => {
it("should return true for empty queries", () => {
expect(isValidTSQLQuery("")).toBe(true);
expect(isValidTSQLQuery(" ")).toBe(true);
expect(isValidTRQLQuery("")).toBe(true);
expect(isValidTRQLQuery(" ")).toBe(true);
});
it("should return true for valid SELECT queries", () => {
expect(isValidTSQLQuery("SELECT * FROM users")).toBe(true);
expect(isValidTSQLQuery("SELECT id, name FROM users WHERE status = 'active'")).toBe(true);
expect(isValidTSQLQuery("SELECT count(*) FROM users GROUP BY status")).toBe(true);
expect(isValidTRQLQuery("SELECT * FROM users")).toBe(true);
expect(isValidTRQLQuery("SELECT id, name FROM users WHERE status = 'active'")).toBe(true);
expect(isValidTRQLQuery("SELECT count(*) FROM users GROUP BY status")).toBe(true);
});
it("should return true for queries with ORDER BY", () => {
expect(isValidTSQLQuery("SELECT * FROM users ORDER BY created_at DESC")).toBe(true);
expect(isValidTRQLQuery("SELECT * FROM users ORDER BY created_at DESC")).toBe(true);
});
it("should return true for queries with LIMIT", () => {
expect(isValidTSQLQuery("SELECT * FROM users LIMIT 10")).toBe(true);
expect(isValidTSQLQuery("SELECT * FROM users LIMIT 10 OFFSET 20")).toBe(true);
expect(isValidTRQLQuery("SELECT * FROM users LIMIT 10")).toBe(true);
expect(isValidTRQLQuery("SELECT * FROM users LIMIT 10 OFFSET 20")).toBe(true);
});
it("should return true for queries with JOINs", () => {
expect(isValidTSQLQuery("SELECT * FROM users JOIN orders ON users.id = orders.user_id")).toBe(
expect(isValidTRQLQuery("SELECT * FROM users JOIN orders ON users.id = orders.user_id")).toBe(
true
);
expect(
isValidTSQLQuery(
isValidTRQLQuery(
"SELECT * FROM users LEFT JOIN orders ON users.id = orders.user_id"
)
).toBe(true);
});
it("should return false for invalid syntax", () => {
expect(isValidTSQLQuery("SELEC * FROM users")).toBe(false);
expect(isValidTSQLQuery("SELECT * FORM users")).toBe(false);
expect(isValidTSQLQuery("SELECT FROM users")).toBe(false);
expect(isValidTRQLQuery("SELEC * FROM users")).toBe(false);
expect(isValidTRQLQuery("SELECT * FORM users")).toBe(false);
expect(isValidTRQLQuery("SELECT FROM users")).toBe(false);
});
it("should return false for incomplete queries", () => {
expect(isValidTSQLQuery("SELECT * FROM")).toBe(false);
expect(isValidTSQLQuery("SELECT")).toBe(false);
expect(isValidTRQLQuery("SELECT * FROM")).toBe(false);
expect(isValidTRQLQuery("SELECT")).toBe(false);
});
});
describe("getTSQLError", () => {
describe("getTRQLError", () => {
it("should return null for empty queries", () => {
expect(getTSQLError("")).toBeNull();
expect(getTSQLError(" ")).toBeNull();
expect(getTRQLError("")).toBeNull();
expect(getTRQLError(" ")).toBeNull();
});
it("should return null for valid queries", () => {
expect(getTSQLError("SELECT * FROM users")).toBeNull();
expect(getTSQLError("SELECT id, name FROM users WHERE id = 1")).toBeNull();
expect(getTRQLError("SELECT * FROM users")).toBeNull();
expect(getTRQLError("SELECT id, name FROM users WHERE id = 1")).toBeNull();
});
it("should return error message for invalid queries", () => {
const error = getTSQLError("SELEC * FROM users");
const error = getTRQLError("SELEC * FROM users");
expect(error).not.toBeNull();
expect(typeof error).toBe("string");
});
it("should include position information in error", () => {
const error = getTSQLError("SELECT * FORM users");
const error = getTRQLError("SELECT * FORM users");
expect(error).not.toBeNull();
// Error message should contain line/column info
expect(error).toContain("line");
});
it("should handle missing FROM clause", () => {
const error = getTSQLError("SELECT * WHERE id = 1");
const error = getTRQLError("SELECT * WHERE id = 1");
expect(error).not.toBeNull();
});
});
@@ -1,12 +1,12 @@
import type { EditorView } from "@codemirror/view";
import type { Diagnostic } from "@codemirror/lint";
import type { TableSchema } from "@internal/tsql";
import { parseTSQLSelect, SyntaxError, QueryError, validateQuery } from "@internal/tsql";
import type { TableSchema } from "@internal/trql";
import { parseTRQLSelect, SyntaxError, QueryError, validateQuery } from "@internal/trql";
/**
* Configuration for the TSQL linter
* Configuration for the TRQL linter
*/
export interface TSQLLinterConfig {
export interface TRQLLinterConfig {
/** Optional schema for validating table/column names */
schema?: TableSchema[];
/** Delay in milliseconds before running the linter (debouncing) */
@@ -14,7 +14,7 @@ export interface TSQLLinterConfig {
}
/**
* Extract line and column from a TSQL error message
* Extract line and column from a TRQL error message
* Error format: "Syntax error at line X:Y: message"
*/
function parseErrorPosition(message: string): { line: number; column: number } | null {
@@ -67,16 +67,16 @@ function findTokenEnd(doc: string, start: number): number {
}
/**
* Create a TSQL linter function for CodeMirror
* Create a TRQL linter function for CodeMirror
*
* This linter uses the TSQL ANTLR parser to detect syntax errors
* This linter uses the TRQL ANTLR parser to detect syntax errors
* and optionally validates against a schema.
*
* @param config - Linter configuration
* @returns A linter function for use with CodeMirror's linter extension
*/
export function createTSQLLinter(
config: TSQLLinterConfig = {}
export function createTRQLLinter(
config: TRQLLinterConfig = {}
): (view: EditorView) => Diagnostic[] {
const { schema = [] } = config;
@@ -92,7 +92,7 @@ export function createTSQLLinter(
try {
// Try to parse the query
const ast = parseTSQLSelect(content);
const ast = parseTRQLSelect(content);
// If parsing succeeds and we have a schema, run schema validation
if (schema.length > 0) {
@@ -112,7 +112,7 @@ export function createTSQLLinter(
to: content.length,
severity,
message: issue.message,
source: "tsql",
source: "trql",
});
}
}
@@ -142,7 +142,7 @@ export function createTSQLLinter(
to,
severity: "error",
message: message,
source: "tsql",
source: "trql",
});
} else if (error instanceof QueryError) {
// Schema validation errors don't have position info,
@@ -152,7 +152,7 @@ export function createTSQLLinter(
to: content.length,
severity: "warning",
message: error.message,
source: "tsql",
source: "trql",
});
} else if (error instanceof Error) {
// Unknown error
@@ -161,7 +161,7 @@ export function createTSQLLinter(
to: content.length,
severity: "error",
message: error.message,
source: "tsql",
source: "trql",
});
}
}
@@ -171,18 +171,18 @@ export function createTSQLLinter(
}
/**
* Check if a TSQL query is valid
* Check if a TRQL query is valid
*
* @param query - The query to validate
* @returns true if the query is valid, false otherwise
*/
export function isValidTSQLQuery(query: string): boolean {
export function isValidTRQLQuery(query: string): boolean {
if (!query.trim()) {
return true; // Empty queries are considered valid
}
try {
parseTSQLSelect(query);
parseTRQLSelect(query);
return true;
} catch {
return false;
@@ -190,18 +190,18 @@ export function isValidTSQLQuery(query: string): boolean {
}
/**
* Get error message for a TSQL query, if any
* Get error message for a TRQL query, if any
*
* @param query - The query to validate
* @returns Error message if invalid, null if valid
*/
export function getTSQLError(query: string): string | null {
export function getTRQLError(query: string): string | null {
if (!query.trim()) {
return null;
}
try {
parseTSQLSelect(query);
parseTRQLSelect(query);
return null;
} catch (error) {
if (error instanceof Error) {
@@ -1,6 +0,0 @@
// TSQL CodeMirror support
// Provides syntax highlighting, autocompletion, and linting for TSQL queries
export { createTSQLCompletion } from "./tsqlCompletion";
export { createTSQLLinter, isValidTSQLQuery, getTSQLError, type TSQLLinterConfig } from "./tsqlLinter";
@@ -5,7 +5,7 @@ import {
PlayIcon,
} from "@heroicons/react/20/solid";
import type { OutputColumnMetadata } from "@internal/clickhouse";
import type { ColumnSchema } from "@internal/tsql";
import type { ColumnSchema } from "@internal/trql";
import { Form, useNavigation } from "@remix-run/react";
import {
type ActionFunctionArgs,
@@ -19,8 +19,8 @@ import { ClockRotateLeftIcon } from "~/assets/icons/ClockRotateLeftIcon";
import { ExitIcon } from "~/assets/icons/ExitIcon";
import { AlphaTitle } from "~/components/AlphaBadge";
import { CodeBlock } from "~/components/code/CodeBlock";
import { TSQLEditor } from "~/components/code/TSQLEditor";
import { TSQLResultsTable } from "~/components/code/TSQLResultsTable";
import { TRQLEditor } from "~/components/code/TRQLEditor";
import { TRQLResultsTable } from "~/components/code/TRQLResultsTable";
import {
ClientTabs,
ClientTabsContent,
@@ -227,7 +227,7 @@ export default function Page() {
<div className="grid max-h-full grid-rows-[auto_1fr] overflow-hidden">
{/* Query editor */}
<div className="flex flex-col gap-2 pb-2">
<TSQLEditor
<TRQLEditor
defaultValue={query}
onChange={setQuery}
schema={querySchemas}
@@ -324,7 +324,7 @@ export default function Page() {
{results.error}
</pre>
) : results?.rows && results?.columns ? (
<TSQLResultsTable
<TRQLResultsTable
rows={results.rows}
columns={results.columns}
prettyFormatting={prettyFormatting}
@@ -1881,7 +1881,7 @@ const SQL_KEYWORDS = [
"MAX",
];
function highlightSQL(query: string): React.ReactNode[] {
function highlightRQL(query: string): React.ReactNode[] {
// Normalize whitespace for display (let CSS line-clamp handle truncation)
const normalized = query.replace(/\s+/g, " ").slice(0, 200);
const suffix = "";
@@ -1962,7 +1962,7 @@ function QueryHistoryPopover({
>
<div className="flex flex-1 flex-col items-start overflow-hidden">
<p className="line-clamp-2 w-full break-words text-left font-mono text-xs text-[#9b99ff]">
{highlightSQL(item.query)}
{highlightRQL(item.query)}
</p>
<div className="flex items-center gap-2 text-xs text-text-dimmed">
<DateTime date={item.createdAt} showTooltip={false} />
@@ -1,6 +1,6 @@
import { useState } from "react";
import { TSQLEditor } from "~/components/code/TSQLEditor";
import { column, type TableSchema } from "@internal/tsql";
import { TRQLEditor } from "~/components/code/TRQLEditor";
import { column, type TableSchema } from "@internal/trql";
const RUN_STATUSES = ["PENDING", "QUEUED", "EXECUTING", "COMPLETED", "FAILED", "CANCELED"] as const;
const LOG_LEVELS = ["DEBUG", "INFO", "WARN", "ERROR"] as const;
@@ -151,7 +151,7 @@ export default function Story() {
return (
<div className="flex flex-col gap-y-8 p-8">
<div>
<h1 className="mb-2 text-2xl font-bold text-text-bright">TSQL Editor</h1>
<h1 className="mb-2 text-2xl font-bold text-text-bright">TRQL Editor</h1>
<p className="text-text-dimmed">
A CodeMirror-based SQL editor with syntax highlighting, schema-aware autocomplete, and
real-time error detection.
@@ -182,7 +182,7 @@ export default function Story() {
suggestions. Available tables: <code>runs</code>, <code>logs</code>
</p>
<div className="overflow-hidden rounded-lg border border-grid-dimmed">
<TSQLEditor
<TRQLEditor
defaultValue={query}
onChange={setQuery}
schema={exampleSchema}
@@ -199,7 +199,7 @@ export default function Story() {
<div className="flex flex-col gap-2">
<h2 className="text-lg font-semibold text-text-bright">Read-only Mode</h2>
<div className="overflow-hidden rounded-lg border border-grid-dimmed">
<TSQLEditor
<TRQLEditor
defaultValue="SELECT id, status, created_at FROM runs WHERE status = 'FAILED' ORDER BY created_at DESC LIMIT 10"
readOnly={true}
schema={exampleSchema}
@@ -218,7 +218,7 @@ export default function Story() {
Editor without schema - still has SQL syntax highlighting and keyword completion.
</p>
<div className="overflow-hidden rounded-lg border border-grid-dimmed">
<TSQLEditor
<TRQLEditor
defaultValue="SELECT * FROM my_table WHERE id = 1"
linterEnabled={true}
showCopyButton={true}
@@ -234,7 +234,7 @@ export default function Story() {
The linter detects syntax errors and underlines them in red.
</p>
<div className="overflow-hidden rounded-lg border border-grid-dimmed">
<TSQLEditor
<TRQLEditor
defaultValue="SELEC * FORM runs"
linterEnabled={true}
showCopyButton={true}
@@ -251,7 +251,7 @@ export default function Story() {
<code>'INVALID_STATUS'</code> to a valid status like <code>'COMPLETED'</code>.
</p>
<div className="overflow-hidden rounded-lg border border-grid-dimmed">
<TSQLEditor
<TRQLEditor
defaultValue="SELECT * FROM runs WHERE status = 'INVALID_STATUS' LIMIT 10"
schema={exampleSchema}
linterEnabled={true}
@@ -269,7 +269,7 @@ export default function Story() {
valid column like <code>status</code>.
</p>
<div className="overflow-hidden rounded-lg border border-grid-dimmed">
<TSQLEditor
<TRQLEditor
defaultValue="SELECT id, unknown_col FROM runs LIMIT 10"
schema={exampleSchema}
linterEnabled={true}
+2 -2
View File
@@ -121,8 +121,8 @@ const stories: Story[] = [
slug: "tree-view",
},
{
name: "TSQL Editor",
slug: "tsql-editor",
name: "TRQL Editor",
slug: "trql-editor",
},
{
name: "Timeline",
@@ -1,17 +1,17 @@
import {
executeTSQL,
type ExecuteTSQLOptions,
executeTRQL,
type ExecuteTRQLOptions,
type FieldMappings,
type TSQLQueryResult,
type TRQLQueryResult,
} from "@internal/clickhouse";
import type { CustomerQuerySource } from "@trigger.dev/database";
import type { TableSchema } from "@internal/tsql";
import type { TableSchema } from "@internal/trql";
import { type z } from "zod";
import { prisma } from "~/db.server";
import { env } from "~/env.server";
import { clickhouseClient } from "./clickhouseInstance.server";
export type { TableSchema, TSQLQueryResult };
export type { TableSchema, TRQLQueryResult };
export type QueryScope = "organization" | "project" | "environment";
@@ -22,7 +22,7 @@ const scopeToEnum = {
} as const;
export type ExecuteQueryOptions<TOut extends z.ZodSchema> = Omit<
ExecuteTSQLOptions<TOut>,
ExecuteTRQLOptions<TOut>,
"tableSchema" | "organizationId" | "projectId" | "environmentId" | "fieldMappings"
> & {
tableSchema: TableSchema[];
@@ -44,12 +44,12 @@ export type ExecuteQueryOptions<TOut extends z.ZodSchema> = Omit<
};
/**
* Execute a TSQL query against ClickHouse with tenant isolation
* Execute a TRQL query against ClickHouse with tenant isolation
* Handles building tenant options, field mappings, and optionally saves to history
*/
export async function executeQuery<TOut extends z.ZodSchema>(
options: ExecuteQueryOptions<TOut>
): Promise<TSQLQueryResult<z.output<TOut>>> {
): Promise<TRQLQueryResult<z.output<TOut>>> {
const { scope, organizationId, projectId, environmentId, history, ...baseOptions } = options;
// Build tenant IDs based on scope
@@ -85,7 +85,7 @@ export async function executeQuery<TOut extends z.ZodSchema>(
environment: Object.fromEntries(environments.map((e) => [e.id, e.slug])),
};
const result = await executeTSQL(clickhouseClient.reader, {
const result = await executeTRQL(clickhouseClient.reader, {
...baseOptions,
...tenantOptions,
fieldMappings,
+1 -1
View File
@@ -1,4 +1,4 @@
import { column, type TableSchema } from "@internal/tsql";
import { column, type TableSchema } from "@internal/trql";
import { runFriendlyStatus, runStatusTitleFromStatus } from "~/components/runs/v3/TaskRunStatus";
/**
+1 -1
View File
@@ -56,7 +56,7 @@
"@heroicons/react": "^2.0.12",
"@internal/cache": "workspace:*",
"@internal/redis": "workspace:*",
"@internal/tsql": "workspace:*",
"@internal/trql": "workspace:*",
"@internal/run-engine": "workspace:*",
"@internal/schedule-engine": "workspace:*",
"@internal/tracing": "workspace:*",
@@ -1,6 +1,6 @@
import { describe, it, expect } from "vitest";
import { createTSQLCompletion } from "~/components/code/tsql/tsqlCompletion";
import type { TableSchema } from "@internal/tsql";
import { createTRQLCompletion } from "~/components/code/trql/trqlCompletion";
import type { TableSchema } from "@internal/trql";
// Helper to create a mock completion context
function createMockContext(doc: string, pos: number, explicit = false) {
@@ -69,8 +69,8 @@ const testSchema: TableSchema[] = [
},
];
describe("createTSQLCompletion", () => {
const completionSource = createTSQLCompletion(testSchema);
describe("createTRQLCompletion", () => {
const completionSource = createTRQLCompletion(testSchema);
it("should return null for empty input without explicit trigger", () => {
const context = createMockContext("", 0, false);
@@ -1,77 +1,77 @@
import { describe, it, expect } from "vitest";
import { isValidTSQLQuery, getTSQLError } from "~/components/code/tsql/tsqlLinter";
import { isValidTRQLQuery, getTRQLError } from "~/components/code/trql/trqlLinter";
describe("tsqlLinter", () => {
describe("isValidTSQLQuery", () => {
describe("trqlLinter", () => {
describe("isValidTRQLQuery", () => {
it("should return true for empty queries", () => {
expect(isValidTSQLQuery("")).toBe(true);
expect(isValidTSQLQuery(" ")).toBe(true);
expect(isValidTRQLQuery("")).toBe(true);
expect(isValidTRQLQuery(" ")).toBe(true);
});
it("should return true for valid SELECT queries", () => {
expect(isValidTSQLQuery("SELECT * FROM users")).toBe(true);
expect(isValidTSQLQuery("SELECT id, name FROM users WHERE status = 'active'")).toBe(true);
expect(isValidTSQLQuery("SELECT count(*) FROM users GROUP BY status")).toBe(true);
expect(isValidTRQLQuery("SELECT * FROM users")).toBe(true);
expect(isValidTRQLQuery("SELECT id, name FROM users WHERE status = 'active'")).toBe(true);
expect(isValidTRQLQuery("SELECT count(*) FROM users GROUP BY status")).toBe(true);
});
it("should return true for queries with ORDER BY", () => {
expect(isValidTSQLQuery("SELECT * FROM users ORDER BY created_at DESC")).toBe(true);
expect(isValidTRQLQuery("SELECT * FROM users ORDER BY created_at DESC")).toBe(true);
});
it("should return true for queries with LIMIT", () => {
expect(isValidTSQLQuery("SELECT * FROM users LIMIT 10")).toBe(true);
expect(isValidTSQLQuery("SELECT * FROM users LIMIT 10 OFFSET 20")).toBe(true);
expect(isValidTRQLQuery("SELECT * FROM users LIMIT 10")).toBe(true);
expect(isValidTRQLQuery("SELECT * FROM users LIMIT 10 OFFSET 20")).toBe(true);
});
it("should return true for queries with JOINs", () => {
expect(isValidTSQLQuery("SELECT * FROM users JOIN orders ON users.id = orders.user_id")).toBe(
expect(isValidTRQLQuery("SELECT * FROM users JOIN orders ON users.id = orders.user_id")).toBe(
true
);
expect(
isValidTSQLQuery(
isValidTRQLQuery(
"SELECT * FROM users LEFT JOIN orders ON users.id = orders.user_id"
)
).toBe(true);
});
it("should return false for invalid syntax", () => {
expect(isValidTSQLQuery("SELEC * FROM users")).toBe(false);
expect(isValidTSQLQuery("SELECT * FORM users")).toBe(false);
expect(isValidTSQLQuery("SELECT FROM users")).toBe(false);
expect(isValidTRQLQuery("SELEC * FROM users")).toBe(false);
expect(isValidTRQLQuery("SELECT * FORM users")).toBe(false);
expect(isValidTRQLQuery("SELECT FROM users")).toBe(false);
});
it("should return false for incomplete queries", () => {
expect(isValidTSQLQuery("SELECT * FROM")).toBe(false);
expect(isValidTSQLQuery("SELECT")).toBe(false);
expect(isValidTRQLQuery("SELECT * FROM")).toBe(false);
expect(isValidTRQLQuery("SELECT")).toBe(false);
});
});
describe("getTSQLError", () => {
describe("getTRQLError", () => {
it("should return null for empty queries", () => {
expect(getTSQLError("")).toBeNull();
expect(getTSQLError(" ")).toBeNull();
expect(getTRQLError("")).toBeNull();
expect(getTRQLError(" ")).toBeNull();
});
it("should return null for valid queries", () => {
expect(getTSQLError("SELECT * FROM users")).toBeNull();
expect(getTSQLError("SELECT id, name FROM users WHERE id = 1")).toBeNull();
expect(getTRQLError("SELECT * FROM users")).toBeNull();
expect(getTRQLError("SELECT id, name FROM users WHERE id = 1")).toBeNull();
});
it("should return error message for invalid queries", () => {
const error = getTSQLError("SELEC * FROM users");
const error = getTRQLError("SELEC * FROM users");
expect(error).not.toBeNull();
expect(typeof error).toBe("string");
});
it("should include position information in error", () => {
const error = getTSQLError("SELECT * FORM users");
const error = getTRQLError("SELECT * FORM users");
expect(error).not.toBeNull();
// Error message should contain line/column info
expect(error).toContain("line");
});
it("should handle unclosed string literals", () => {
const error = getTSQLError("SELECT * FROM users WHERE name = 'test");
const error = getTRQLError("SELECT * FROM users WHERE name = 'test");
expect(error).not.toBeNull();
});
});
+1 -1
View File
@@ -8,7 +8,7 @@
"dependencies": {
"@clickhouse/client": "^1.12.1",
"@internal/tracing": "workspace:*",
"@internal/tsql": "workspace:*",
"@internal/trql": "workspace:*",
"@trigger.dev/core": "workspace:*",
"zod": "3.25.76",
"zod-error": "1.5.0"
@@ -1,34 +1,34 @@
/**
* TSQL Query Execution for ClickHouse
* TRQL Query Execution for ClickHouse
*
* This module provides a safe interface for executing TSQL queries against ClickHouse
* This module provides a safe interface for executing TRQL queries against ClickHouse
* with automatic tenant isolation and SQL injection protection.
*/
import type { ClickHouseSettings } from "@clickhouse/client";
import { z } from "zod";
import {
compileTSQL,
compileTRQL,
transformResults,
type TableSchema,
type QuerySettings,
type FieldMappings,
} from "@internal/tsql";
} from "@internal/trql";
import type { ClickhouseReader, QueryStats } from "./types.js";
import { QueryError } from "./errors.js";
import type { OutputColumnMetadata } from "@internal/tsql";
import type { OutputColumnMetadata } from "@internal/trql";
export type { QueryStats };
export type { TableSchema, QuerySettings, FieldMappings };
/**
* Options for executing a TSQL query
* Options for executing a TRQL query
*/
export interface ExecuteTSQLOptions<TOut extends z.ZodSchema> {
export interface ExecuteTRQLOptions<TOut extends z.ZodSchema> {
/** The name of the operation (for logging/tracing) */
name: string;
/** The TSQL query string to execute */
/** The TRQL query string to execute */
query: string;
/** The Zod schema for validating output rows */
schema: TOut;
@@ -42,7 +42,7 @@ export interface ExecuteTSQLOptions<TOut extends z.ZodSchema> {
tableSchema: TableSchema[];
/** Optional ClickHouse query settings */
clickhouseSettings?: ClickHouseSettings;
/** Optional TSQL query settings (maxRows, timezone, etc.) */
/** Optional TRQL query settings (maxRows, timezone, etc.) */
querySettings?: Partial<QuerySettings>;
/**
* Whether to transform result values using the schema's valueMap
@@ -66,29 +66,29 @@ export interface ExecuteTSQLOptions<TOut extends z.ZodSchema> {
}
/**
* Successful result from TSQL query execution
* Successful result from TRQL query execution
*/
export interface TSQLQuerySuccess<T> {
export interface TRQLQuerySuccess<T> {
rows: T[];
columns: OutputColumnMetadata[];
stats: QueryStats;
}
/**
* Result type for TSQL query execution
* Result type for TRQL query execution
*/
export type TSQLQueryResult<T> = [QueryError, null] | [null, TSQLQuerySuccess<T>];
export type TRQLQueryResult<T> = [QueryError, null] | [null, TRQLQuerySuccess<T>];
/**
* Execute a TSQL query against ClickHouse
* Execute a TRQL query against ClickHouse
*
* This function:
* 1. Compiles the TSQL query to ClickHouse SQL (parse, validate, inject tenant guards)
* 1. Compiles the TRQL query to ClickHouse SQL (parse, validate, inject tenant guards)
* 2. Executes the query and returns validated results
*
* @example
* ```typescript
* const [error, rows] = await executeTSQL(reader, {
* const [error, rows] = await executeTRQL(reader, {
* name: "get_task_runs",
* query: "SELECT id, status FROM task_runs WHERE status = 'completed' ORDER BY created_at DESC LIMIT 100",
* schema: z.object({ id: z.string(), status: z.string() }),
@@ -99,15 +99,15 @@ export type TSQLQueryResult<T> = [QueryError, null] | [null, TSQLQuerySuccess<T>
* });
* ```
*/
export async function executeTSQL<TOut extends z.ZodSchema>(
export async function executeTRQL<TOut extends z.ZodSchema>(
reader: ClickhouseReader,
options: ExecuteTSQLOptions<TOut>
): Promise<TSQLQueryResult<z.output<TOut>>> {
options: ExecuteTRQLOptions<TOut>
): Promise<TRQLQueryResult<z.output<TOut>>> {
const shouldTransformValues = options.transformValues ?? true;
try {
// 1. Compile the TSQL query to ClickHouse SQL
const { sql, params, columns } = compileTSQL(options.query, {
// 1. Compile the TRQL query to ClickHouse SQL
const { sql, params, columns } = compileTRQL(options.query, {
organizationId: options.organizationId,
projectId: options.projectId,
environmentId: options.environmentId,
@@ -148,18 +148,18 @@ export async function executeTSQL<TOut extends z.ZodSchema>(
if (error instanceof Error) {
return [new QueryError(error.message, { query: options.query }), null];
}
return [new QueryError("Unknown error executing TSQL query", { query: options.query }), null];
return [new QueryError("Unknown error executing TRQL query", { query: options.query }), null];
}
}
/**
* Create a reusable TSQL query executor bound to specific table schemas
* Create a reusable TRQL query executor bound to specific table schemas
*
* @example
* ```typescript
* const tsqlExecutor = createTSQLExecutor(reader, [taskRunsSchema, taskEventsSchema]);
* const trqlExecutor = createTRQLExecutor(reader, [taskRunsSchema, taskEventsSchema]);
*
* const [error, rows] = await tsqlExecutor.execute({
* const [error, rows] = await trqlExecutor.execute({
* name: "get_task_runs",
* query: "SELECT * FROM task_runs LIMIT 10",
* schema: taskRunRowSchema,
@@ -169,12 +169,12 @@ export async function executeTSQL<TOut extends z.ZodSchema>(
* });
* ```
*/
export function createTSQLExecutor(reader: ClickhouseReader, tableSchema: TableSchema[]) {
export function createTRQLExecutor(reader: ClickhouseReader, tableSchema: TableSchema[]) {
return {
execute: <TOut extends z.ZodSchema>(
options: Omit<ExecuteTSQLOptions<TOut>, "tableSchema">
): Promise<TSQLQueryResult<z.output<TOut>>> => {
return executeTSQL(reader, { ...options, tableSchema });
options: Omit<ExecuteTRQLOptions<TOut>, "tableSchema">
): Promise<TRQLQueryResult<z.output<TOut>>> => {
return executeTRQL(reader, { ...options, tableSchema });
},
};
}
+8 -8
View File
@@ -31,18 +31,18 @@ export type * from "./taskRuns.js";
export type * from "./taskEvents.js";
export type * from "./client/queryBuilder.js";
// TSQL query execution
// TRQL query execution
export {
executeTSQL,
createTSQLExecutor,
type ExecuteTSQLOptions,
executeTRQL,
createTRQLExecutor,
type ExecuteTRQLOptions,
type TableSchema,
type TSQLQueryResult,
type TSQLQuerySuccess,
type TRQLQueryResult,
type TRQLQuerySuccess,
type QueryStats,
type FieldMappings,
} from "./client/tsql.js";
export type { OutputColumnMetadata } from "@internal/tsql";
} from "./client/trql.js";
export type { OutputColumnMetadata } from "@internal/trql";
export type ClickhouseCommonConfig = {
keepAlive?: {
@@ -1,12 +1,12 @@
import { clickhouseTest } from "@internal/testcontainers";
import { z } from "zod";
import { ClickhouseClient } from "./client/client.js";
import { executeTSQL, createTSQLExecutor, type TableSchema } from "./client/tsql.js";
import { executeTRQL, createTRQLExecutor, type TableSchema } from "./client/trql.js";
import { insertTaskRuns } from "./taskRuns.js";
import { column } from "@internal/tsql";
import { column } from "@internal/trql";
/**
* Schema definition for task_runs table used in TSQL tests
* Schema definition for task_runs table used in TRQL tests
*/
const taskRunsSchema: TableSchema = {
name: "task_runs",
@@ -84,7 +84,7 @@ function createTaskRun(overrides: Partial<typeof defaultTaskRun> = {}) {
};
}
describe("TSQL Integration Tests", () => {
describe("TRQL Integration Tests", () => {
clickhouseTest("should execute a simple SELECT query", async ({ clickhouseContainer }) => {
const client = new ClickhouseClient({
name: "test",
@@ -101,8 +101,8 @@ describe("TSQL Integration Tests", () => {
]);
expect(insertError).toBeNull();
// Execute TSQL query
const [error, result] = await executeTSQL(client, {
// Execute TRQL query
const [error, result] = await executeTRQL(client, {
name: "test-simple-select",
query: "SELECT run_id, status FROM task_runs",
schema: z.object({ run_id: z.string(), status: z.string() }),
@@ -141,7 +141,7 @@ describe("TSQL Integration Tests", () => {
createTaskRun({ run_id: "run_filter3", status: "COMPLETED_SUCCESSFULLY" }),
]);
const [error, result] = await executeTSQL(client, {
const [error, result] = await executeTRQL(client, {
name: "test-where-clause",
query: "SELECT run_id, status FROM task_runs WHERE status = 'COMPLETED_SUCCESSFULLY'",
schema: z.object({ run_id: z.string(), status: z.string() }),
@@ -193,7 +193,7 @@ describe("TSQL Integration Tests", () => {
]);
// Query as tenant1 - should only see tenant1's data
const [error1, result1] = await executeTSQL(client, {
const [error1, result1] = await executeTRQL(client, {
name: "test-tenant-isolation-1",
query: "SELECT run_id FROM task_runs",
schema: z.object({ run_id: z.string() }),
@@ -208,7 +208,7 @@ describe("TSQL Integration Tests", () => {
expect(result1?.rows?.map((r) => r.run_id).sort()).toEqual(["run_tenant1_a", "run_tenant1_b"]);
// Query as tenant2 - should only see tenant2's data
const [error2, result2] = await executeTSQL(client, {
const [error2, result2] = await executeTRQL(client, {
name: "test-tenant-isolation-2",
query: "SELECT run_id FROM task_runs",
schema: z.object({ run_id: z.string() }),
@@ -250,7 +250,7 @@ describe("TSQL Integration Tests", () => {
]);
// Attacker tries to access victim's data with OR 1=1
const [error, result] = await executeTSQL(client, {
const [error, result] = await executeTRQL(client, {
name: "test-cross-tenant-attack",
query: "SELECT run_id, status FROM task_runs WHERE status = 'COMPLETED' OR 1=1",
schema: z.object({ run_id: z.string(), status: z.string() }),
@@ -283,7 +283,7 @@ describe("TSQL Integration Tests", () => {
createTaskRun({ run_id: "run_agg4", status: "FAILED" }),
]);
const [error, result] = await executeTSQL(client, {
const [error, result] = await executeTRQL(client, {
name: "test-aggregation",
query:
"SELECT status, count(*) as cnt FROM task_runs GROUP BY status ORDER BY cnt DESC, status ASC",
@@ -321,7 +321,7 @@ describe("TSQL Integration Tests", () => {
createTaskRun({ run_id: "run_order3", created_at: now - 2000 }),
]);
const [error, result] = await executeTSQL(client, {
const [error, result] = await executeTRQL(client, {
name: "test-order-limit",
query: "SELECT run_id FROM task_runs ORDER BY created_at DESC LIMIT 2",
schema: z.object({ run_id: z.string() }),
@@ -343,7 +343,7 @@ describe("TSQL Integration Tests", () => {
url: clickhouseContainer.getConnectionUrl(),
});
const [error, result] = await executeTSQL(client, {
const [error, result] = await executeTRQL(client, {
name: "test-unknown-table",
query: "SELECT * FROM unknown_table",
schema: z.object({ id: z.string() }),
@@ -358,7 +358,7 @@ describe("TSQL Integration Tests", () => {
expect(result).toBeNull();
});
clickhouseTest("should work with createTSQLExecutor", async ({ clickhouseContainer }) => {
clickhouseTest("should work with createTRQLExecutor", async ({ clickhouseContainer }) => {
const client = new ClickhouseClient({
name: "test",
url: clickhouseContainer.getConnectionUrl(),
@@ -372,9 +372,9 @@ describe("TSQL Integration Tests", () => {
]);
// Create a reusable executor
const tsql = createTSQLExecutor(client, [taskRunsSchema]);
const trql = createTRQLExecutor(client, [taskRunsSchema]);
const [error, result] = await tsql.execute({
const [error, result] = await trql.execute({
name: "test-executor",
query: "SELECT run_id, status FROM task_runs WHERE status = 'PENDING'",
schema: z.object({ run_id: z.string(), status: z.string() }),
@@ -402,7 +402,7 @@ describe("TSQL Integration Tests", () => {
]);
// Query with a "malicious" value that looks like SQL
const [error, result] = await executeTSQL(client, {
const [error, result] = await executeTRQL(client, {
name: "test-injection",
query: "SELECT run_id, status FROM task_runs WHERE status = 'DROP TABLE task_runs'",
schema: z.object({ run_id: z.string(), status: z.string() }),
@@ -433,7 +433,7 @@ describe("TSQL Integration Tests", () => {
createTaskRun({ run_id: "run_in4", status: "CANCELLED" }),
]);
const [error, result] = await executeTSQL(client, {
const [error, result] = await executeTRQL(client, {
name: "test-in-query",
query:
"SELECT run_id, status FROM task_runs WHERE status IN ('COMPLETED_SUCCESSFULLY', 'FAILED')",
@@ -463,7 +463,7 @@ describe("TSQL Integration Tests", () => {
createTaskRun({ run_id: "run_like3", task_identifier: "sms/send" }),
]);
const [error, result] = await executeTSQL(client, {
const [error, result] = await executeTRQL(client, {
name: "test-like-query",
query: "SELECT run_id, task_identifier FROM task_runs WHERE task_identifier LIKE 'email%'",
schema: z.object({ run_id: z.string(), task_identifier: z.string() }),
@@ -479,7 +479,7 @@ describe("TSQL Integration Tests", () => {
});
});
describe("TSQL Optional Tenant Filter Tests", () => {
describe("TRQL Optional Tenant Filter Tests", () => {
clickhouseTest(
"should query across all projects when projectId is omitted",
async ({ clickhouseContainer }) => {
@@ -526,7 +526,7 @@ describe("TSQL Optional Tenant Filter Tests", () => {
]);
// Query across all projects (omit projectId and environmentId)
const [error, result] = await executeTSQL(client, {
const [error, result] = await executeTRQL(client, {
name: "test-cross-project-query",
query: "SELECT run_id FROM task_runs",
schema: z.object({ run_id: z.string() }),
@@ -586,7 +586,7 @@ describe("TSQL Optional Tenant Filter Tests", () => {
]);
// Query across all environments (omit environmentId only)
const [error, result] = await executeTSQL(client, {
const [error, result] = await executeTRQL(client, {
name: "test-cross-env-query",
query: "SELECT run_id FROM task_runs",
schema: z.object({ run_id: z.string() }),
@@ -645,7 +645,7 @@ describe("TSQL Optional Tenant Filter Tests", () => {
]);
// Query org1 across all projects - should NOT see org2's data
const [error1, result1] = await executeTSQL(client, {
const [error1, result1] = await executeTRQL(client, {
name: "test-org-isolation-1",
query: "SELECT run_id FROM task_runs",
schema: z.object({ run_id: z.string() }),
@@ -659,7 +659,7 @@ describe("TSQL Optional Tenant Filter Tests", () => {
expect(result1?.rows?.map((r) => r.run_id).sort()).toEqual(["run_org1_a", "run_org1_b"]);
// Query org2 across all projects - should NOT see org1's data
const [error2, result2] = await executeTSQL(client, {
const [error2, result2] = await executeTRQL(client, {
name: "test-org-isolation-2",
query: "SELECT run_id FROM task_runs",
schema: z.object({ run_id: z.string() }),
@@ -702,7 +702,7 @@ describe("TSQL Optional Tenant Filter Tests", () => {
]);
// Attacker tries to use OR 1=1 to bypass org filter
const [error, result] = await executeTSQL(client, {
const [error, result] = await executeTRQL(client, {
name: "test-or-bypass-attempt",
query: "SELECT run_id, status FROM task_runs WHERE status = 'COMPLETED' OR 1=1",
schema: z.object({ run_id: z.string(), status: z.string() }),
@@ -720,7 +720,7 @@ describe("TSQL Optional Tenant Filter Tests", () => {
);
clickhouseTest(
"should work with createTSQLExecutor and optional filters",
"should work with createTRQLExecutor and optional filters",
async ({ clickhouseContainer }) => {
const client = new ClickhouseClient({
name: "test",
@@ -744,10 +744,10 @@ describe("TSQL Optional Tenant Filter Tests", () => {
}),
]);
const tsql = createTSQLExecutor(client, [taskRunsSchema]);
const trql = createTRQLExecutor(client, [taskRunsSchema]);
// Use executor with org-only filter
const [error, result] = await tsql.execute({
const [error, result] = await trql.execute({
name: "test-executor-optional",
query: "SELECT run_id FROM task_runs",
schema: z.object({ run_id: z.string() }),
@@ -762,7 +762,7 @@ describe("TSQL Optional Tenant Filter Tests", () => {
);
});
describe("TSQL Virtual Column Tests", () => {
describe("TRQL Virtual Column Tests", () => {
/**
* Schema with virtual (computed) columns
*/
@@ -831,7 +831,7 @@ describe("TSQL Virtual Column Tests", () => {
}),
]);
const [error, result] = await executeTSQL(client, {
const [error, result] = await executeTRQL(client, {
name: "test-virtual-column-select",
query: "SELECT run_id, execution_duration, usage_duration_seconds FROM task_runs",
schema: z.object({
@@ -885,7 +885,7 @@ describe("TSQL Virtual Column Tests", () => {
]);
// Query runs with execution_duration > 5000ms (5 seconds)
const [error, result] = await executeTSQL(client, {
const [error, result] = await executeTRQL(client, {
name: "test-virtual-column-where",
query: "SELECT run_id FROM task_runs WHERE execution_duration > 5000",
schema: z.object({ run_id: z.string() }),
@@ -927,7 +927,7 @@ describe("TSQL Virtual Column Tests", () => {
]);
// Order by usage_duration_seconds descending (virtual column)
const [error, result] = await executeTSQL(client, {
const [error, result] = await executeTRQL(client, {
name: "test-virtual-column-order",
query:
"SELECT run_id, usage_duration_seconds FROM task_runs ORDER BY usage_duration_seconds DESC",
@@ -970,7 +970,7 @@ describe("TSQL Virtual Column Tests", () => {
]);
// Use virtual column with custom alias
const [error, result] = await executeTSQL(client, {
const [error, result] = await executeTRQL(client, {
name: "test-virtual-column-alias",
query: "SELECT run_id, usage_duration_seconds AS dur_sec FROM task_runs",
schema: z.object({
@@ -1006,7 +1006,7 @@ describe("TSQL Virtual Column Tests", () => {
}),
]);
const [error, result] = await executeTSQL(client, {
const [error, result] = await executeTRQL(client, {
name: "test-virtual-column-null",
query: "SELECT run_id, execution_duration FROM task_runs",
schema: z.object({
@@ -1106,7 +1106,7 @@ describe("TSQL Virtual Column Tests", () => {
// Query runs where invocation_cost > 1.0 (base_cost_in_cents > 100)
// Should return run_medium (2.0) and run_expensive (5.0)
const [error, result] = await executeTSQL(client, {
const [error, result] = await executeTRQL(client, {
name: "test-expression-division-where",
query: "SELECT run_id, invocation_cost FROM task_runs WHERE invocation_cost > 1.0",
schema: z.object({ run_id: z.string(), invocation_cost: z.number() }),
@@ -1149,7 +1149,7 @@ describe("TSQL Virtual Column Tests", () => {
// Query runs where invocation_cost >= 1.0
// Should return run_1 (1.0) and run_2 (2.0)
const [error, result] = await executeTSQL(client, {
const [error, result] = await executeTRQL(client, {
name: "test-expression-gte-where",
query: "SELECT run_id, invocation_cost FROM task_runs WHERE invocation_cost >= 1.0",
schema: z.object({ run_id: z.string(), invocation_cost: z.number() }),
@@ -1188,7 +1188,7 @@ describe("TSQL Virtual Column Tests", () => {
// Query runs where invocation_cost < 1.0
// Should return only run_small (0.5)
const [error, result] = await executeTSQL(client, {
const [error, result] = await executeTRQL(client, {
name: "test-expression-lt-where",
query: "SELECT run_id, invocation_cost FROM task_runs WHERE invocation_cost < 1.0",
schema: z.object({ run_id: z.string(), invocation_cost: z.number() }),
@@ -1231,7 +1231,7 @@ describe("TSQL Virtual Column Tests", () => {
// Query runs where invocation_cost is between 1.0 and 2.0
// Should return only run_mid (1.5)
const [error, result] = await executeTSQL(client, {
const [error, result] = await executeTRQL(client, {
name: "test-expression-between-where",
query:
"SELECT run_id, invocation_cost FROM task_runs WHERE invocation_cost BETWEEN 1.0 AND 2.0",
@@ -1277,7 +1277,7 @@ describe("TSQL Virtual Column Tests", () => {
]);
// Query completed runs with invocation_cost > 2.0
const [error, result] = await executeTSQL(client, {
const [error, result] = await executeTRQL(client, {
name: "test-expression-complex-where",
query:
"SELECT run_id FROM task_runs WHERE status = 'COMPLETED_SUCCESSFULLY' AND invocation_cost > 2.0",
@@ -1324,7 +1324,7 @@ describe("TSQL Virtual Column Tests", () => {
// Query runs where invocation_cost > 100
// Should only return run_large_cost (150.0)
const [error, result] = await executeTSQL(client, {
const [error, result] = await executeTRQL(client, {
name: "test-expression-large-integer-where",
query: "SELECT run_id, invocation_cost FROM task_runs WHERE invocation_cost > 100",
schema: z.object({ run_id: z.string(), invocation_cost: z.number() }),
@@ -1388,8 +1388,8 @@ describe("Field Mapping Tests", () => {
}),
]);
// Execute TSQL query with field mappings
const [error, result] = await executeTSQL(client, {
// Execute TRQL query with field mappings
const [error, result] = await executeTRQL(client, {
name: "test-field-mapping-select",
query: "SELECT run_id, project_ref FROM task_runs",
schema: z.object({ run_id: z.string(), project_ref: z.string().nullable() }),
@@ -1430,7 +1430,7 @@ describe("Field Mapping Tests", () => {
]);
// Execute with an empty mapping (no project_id mapped)
const [error, result] = await executeTSQL(client, {
const [error, result] = await executeTRQL(client, {
name: "test-field-mapping-unmapped",
query: "SELECT run_id, project_ref FROM task_runs WHERE run_id = 'run_fm_unmapped'",
schema: z.object({ run_id: z.string(), project_ref: z.string().nullable() }),
@@ -1477,7 +1477,7 @@ describe("Field Mapping Tests", () => {
]);
// Query using external project_ref value in WHERE clause
const [error, result] = await executeTSQL(client, {
const [error, result] = await executeTRQL(client, {
name: "test-field-mapping-where",
query: "SELECT run_id FROM task_runs WHERE project_ref = 'my-project-ref'",
schema: z.object({ run_id: z.string() }),
@@ -1525,7 +1525,7 @@ describe("Field Mapping Tests", () => {
]);
// Query using IN clause with external project_ref values
const [error, result] = await executeTSQL(client, {
const [error, result] = await executeTRQL(client, {
name: "test-field-mapping-in",
query:
"SELECT run_id FROM task_runs WHERE project_ref IN ('my-project-ref', 'other-project')",
@@ -2402,7 +2402,7 @@ enum CustomerQueryScope {
model CustomerQuery {
id String @id @default(cuid())
/// The TSQL query text that was executed
/// The TRQL query text that was executed
query String
/// The scope of the query (determines which tenant IDs were used)
@@ -1,4 +1,4 @@
# TSQL (TriggerSQL)
# TRQL (TriggerSQL)
TriggerSQL is a DSL that is safely converted into ClickHouse SQL queries with protection against SQL injection and it's tenant-safe (users can only query their own data).
@@ -1,5 +1,5 @@
{
"name": "@internal/tsql",
"name": "@internal/trql",
"private": true,
"version": "0.0.1",
"main": "./src/index.ts",
@@ -13,7 +13,7 @@
"scripts": {
"typecheck": "tsc --noEmit",
"grammar:build": "pnpm grammar:build:typescript",
"grammar:build:typescript": "cat src/grammar/TSQLLexer.typescript.g4 > src/grammar/TSQLLexer.g4 && tail -n +2 src/grammar/TSQLLexer.common.g4 |sed s/isOpeningTag/self.isOpeningTag/ >> src/grammar/TSQLLexer.g4 && antlr4ts src/grammar/TSQLLexer.g4 && rm src/grammar/TSQLLexer.g4 && antlr4ts -visitor -no-listener -Dlanguage=TypeScript src/grammar/TSQLParser.g4",
"grammar:build:typescript": "cat src/grammar/TRQLLexer.typescript.g4 > src/grammar/TRQLLexer.g4 && tail -n +2 src/grammar/TRQLLexer.common.g4 |sed s/isOpeningTag/self.isOpeningTag/ >> src/grammar/TRQLLexer.g4 && antlr4ts src/grammar/TRQLLexer.g4 && rm src/grammar/TRQLLexer.g4 && antlr4ts -visitor -no-listener -Dlanguage=TypeScript src/grammar/TRQLParser.g4",
"test": "vitest --sequence.concurrent=false --no-file-parallelism",
"test:coverage": "vitest --sequence.concurrent=false --no-file-parallelism --coverage.enabled"
},
@@ -1,6 +1,6 @@
lexer grammar TSQLLexer;
lexer grammar TRQLLexer;
// NB! We cat TSQLLexter.typescript.g4 when generating the grammar.
// NB! We cat TRQLLexter.typescript.g4 when generating the grammar.
// NOTE: don't forget to add new keywords to the parser rule "keyword"!
@@ -202,8 +202,8 @@ LBRACE: '{' -> pushMode(DEFAULT_MODE);
LBRACKET: '[';
LPAREN: '(';
LT_EQ: '<=';
TAG_LT_SLASH: '</' -> type(LT_SLASH), pushMode(TSQLX_TAG_CLOSE);
TAG_LT_OPEN: '<' {isOpeningTag()}? -> type(LT), pushMode(TSQLX_TAG_OPEN);
TAG_LT_SLASH: '</' -> type(LT_SLASH), pushMode(TRQLX_TAG_CLOSE);
TAG_LT_OPEN: '<' {isOpeningTag()}? -> type(LT), pushMode(TRQLX_TAG_OPEN);
LT: '<';
LT_SLASH: '</';
NOT_EQ: '!=' | '<>';
@@ -247,11 +247,11 @@ mode IN_FULL_TEMPLATE_STRING;
FULL_STRING_TEXT: ((~([{])) | ESCAPE_CHAR_COMMON | (BACKSLASH LBRACE))+;
FULL_STRING_ESCAPE_TRIGGER: LBRACE -> pushMode(DEFAULT_MODE);
// ───────── TSQLX TAG MODE for opening/self-closing tags ─────────
mode TSQLX_TAG_OPEN;
// ───────── TRQLX TAG MODE for opening/self-closing tags ─────────
mode TRQLX_TAG_OPEN;
TAG_SELF_CLOSE_GT : '/>' -> type(SLASH_GT), popMode; // <tag …/>
TAG_OPEN_GT : '>' -> type(GT), popMode, pushMode(TSQLX_TEXT); // <tag …>
TAG_OPEN_GT : '>' -> type(GT), popMode, pushMode(TRQLX_TEXT); // <tag …>
// minimal token set; map everything back to the default token types
TAG_IDENT : [a-zA-Z_][a-zA-Z0-9_-]* -> type(IDENTIFIER);
@@ -261,28 +261,28 @@ TAG_WS : [ \t\r\n]+ -> channel(HIDDEN);
TAG_LBRACE : '{' -> type(LBRACE), pushMode(DEFAULT_MODE);
// ───────── TSQLX TAG MODE for closing tags ─────────
mode TSQLX_TAG_CLOSE;
// ───────── TRQLX TAG MODE for closing tags ─────────
mode TRQLX_TAG_CLOSE;
TAGC_GT : '>' -> type(GT), popMode; // *** no TEXT push ***
TAGC_IDENT : [a-zA-Z_][a-zA-Z0-9_-]* -> type(IDENTIFIER);
TAGC_WS : [ \t\r\n]+ -> channel(HIDDEN);
// ───────── TSQLX TEXT MODE ─────────
mode TSQLX_TEXT;
// ───────── TRQLX TEXT MODE ─────────
mode TRQLX_TEXT;
TSQLX_TEXT_TEXT
TRQLX_TEXT_TEXT
: ~[<{]+ ; // everything except “{” or “<”
TSQLX_TEXT_LBRACE
TRQLX_TEXT_LBRACE
: '{' -> type(LBRACE), pushMode(DEFAULT_MODE);
TSQLX_TEXT_LT_SLASH
: '</' -> type(LT_SLASH), popMode, pushMode(TSQLX_TAG_CLOSE);
TRQLX_TEXT_LT_SLASH
: '</' -> type(LT_SLASH), popMode, pushMode(TRQLX_TAG_CLOSE);
TSQLX_TEXT_LT
: '<' -> type(LT), pushMode(TSQLX_TAG_OPEN);
TRQLX_TEXT_LT
: '<' -> type(LT), pushMode(TRQLX_TAG_OPEN);
TSQLX_TEXT_WS
TRQLX_TEXT_WS
: [ \t\r\n]+ -> channel(HIDDEN);
File diff suppressed because one or more lines are too long
@@ -164,8 +164,8 @@ FULL_STRING_TEXT=163
FULL_STRING_ESCAPE_TRIGGER=164
TAG_WS=165
TAGC_WS=166
TSQLX_TEXT_TEXT=167
TSQLX_TEXT_WS=168
TRQLX_TEXT_TEXT=167
TRQLX_TEXT_WS=168
'->'=114
'*'=115
'`'=116
@@ -1,4 +1,4 @@
// Generated from src/grammar/TSQLLexer.g4 by ANTLR 4.9.0-SNAPSHOT
// Generated from src/grammar/TRQLLexer.g4 by ANTLR 4.9.0-SNAPSHOT
// put any global imports you need here
@@ -15,7 +15,7 @@ import { VocabularyImpl } from "antlr4ts/VocabularyImpl";
import * as Utils from "antlr4ts/misc/Utils";
export class TSQLLexer extends Lexer {
export class TRQLLexer extends Lexer {
public static readonly ALL = 1;
public static readonly AND = 2;
public static readonly ANTI = 3;
@@ -182,13 +182,13 @@ export class TSQLLexer extends Lexer {
public static readonly FULL_STRING_ESCAPE_TRIGGER = 164;
public static readonly TAG_WS = 165;
public static readonly TAGC_WS = 166;
public static readonly TSQLX_TEXT_TEXT = 167;
public static readonly TSQLX_TEXT_WS = 168;
public static readonly TRQLX_TEXT_TEXT = 167;
public static readonly TRQLX_TEXT_WS = 168;
public static readonly IN_TEMPLATE_STRING = 1;
public static readonly IN_FULL_TEMPLATE_STRING = 2;
public static readonly TSQLX_TAG_OPEN = 3;
public static readonly TSQLX_TAG_CLOSE = 4;
public static readonly TSQLX_TEXT = 5;
public static readonly TRQLX_TAG_OPEN = 3;
public static readonly TRQLX_TAG_CLOSE = 4;
public static readonly TRQLX_TEXT = 5;
// tslint:disable:no-trailing-whitespace
public static readonly channelNames: string[] = ["DEFAULT_TOKEN_CHANNEL", "HIDDEN"];
@@ -198,9 +198,9 @@ export class TSQLLexer extends Lexer {
"DEFAULT_MODE",
"IN_TEMPLATE_STRING",
"IN_FULL_TEMPLATE_STRING",
"TSQLX_TAG_OPEN",
"TSQLX_TAG_CLOSE",
"TSQLX_TEXT",
"TRQLX_TAG_OPEN",
"TRQLX_TAG_CLOSE",
"TRQLX_TEXT",
];
public static readonly ruleNames: string[] = [
@@ -411,11 +411,11 @@ export class TSQLLexer extends Lexer {
"TAGC_GT",
"TAGC_IDENT",
"TAGC_WS",
"TSQLX_TEXT_TEXT",
"TSQLX_TEXT_LBRACE",
"TSQLX_TEXT_LT_SLASH",
"TSQLX_TEXT_LT",
"TSQLX_TEXT_WS",
"TRQLX_TEXT_TEXT",
"TRQLX_TEXT_LBRACE",
"TRQLX_TEXT_LT_SLASH",
"TRQLX_TEXT_LT",
"TRQLX_TEXT_WS",
];
private static readonly _LITERAL_NAMES: Array<string | undefined> = [
@@ -746,19 +746,19 @@ export class TSQLLexer extends Lexer {
"FULL_STRING_ESCAPE_TRIGGER",
"TAG_WS",
"TAGC_WS",
"TSQLX_TEXT_TEXT",
"TSQLX_TEXT_WS",
"TRQLX_TEXT_TEXT",
"TRQLX_TEXT_WS",
];
public static readonly VOCABULARY: Vocabulary = new VocabularyImpl(
TSQLLexer._LITERAL_NAMES,
TSQLLexer._SYMBOLIC_NAMES,
TRQLLexer._LITERAL_NAMES,
TRQLLexer._SYMBOLIC_NAMES,
[]
);
// @Override
// @NotNull
public get vocabulary(): Vocabulary {
return TSQLLexer.VOCABULARY;
return TRQLLexer.VOCABULARY;
}
// tslint:enable:no-trailing-whitespace
@@ -841,32 +841,32 @@ export class TSQLLexer extends Lexer {
constructor(input: CharStream) {
super(input);
this._interp = new LexerATNSimulator(TSQLLexer._ATN, this);
this._interp = new LexerATNSimulator(TRQLLexer._ATN, this);
}
// @Override
public get grammarFileName(): string {
return "TSQLLexer.g4";
return "TRQLLexer.g4";
}
// @Override
public get ruleNames(): string[] {
return TSQLLexer.ruleNames;
return TRQLLexer.ruleNames;
}
// @Override
public get serializedATN(): string {
return TSQLLexer._serializedATN;
return TRQLLexer._serializedATN;
}
// @Override
public get channelNames(): string[] {
return TSQLLexer.channelNames;
return TRQLLexer.channelNames;
}
// @Override
public get modeNames(): string[] {
return TSQLLexer.modeNames;
return TRQLLexer.modeNames;
}
// @Override
@@ -1711,20 +1711,20 @@ export class TSQLLexer extends Lexer {
"\x02\ts\x02\t\x85\x02";
public static readonly _serializedATN: string = Utils.join(
[
TSQLLexer._serializedATNSegment0,
TSQLLexer._serializedATNSegment1,
TSQLLexer._serializedATNSegment2,
TRQLLexer._serializedATNSegment0,
TRQLLexer._serializedATNSegment1,
TRQLLexer._serializedATNSegment2,
],
""
);
public static __ATN: ATN;
public static get _ATN(): ATN {
if (!TSQLLexer.__ATN) {
TSQLLexer.__ATN = new ATNDeserializer().deserialize(
Utils.toCharArray(TSQLLexer._serializedATN)
if (!TRQLLexer.__ATN) {
TRQLLexer.__ATN = new ATNDeserializer().deserialize(
Utils.toCharArray(TRQLLexer._serializedATN)
);
}
return TSQLLexer.__ATN;
return TRQLLexer.__ATN;
}
}
@@ -1,4 +1,4 @@
lexer grammar TSQLLexer;
lexer grammar TRQLLexer;
@header {
// put any global imports you need here
@@ -1,7 +1,7 @@
parser grammar TSQLParser;
parser grammar TRQLParser;
options {
tokenVocab = TSQLLexer;
tokenVocab = TRQLLexer;
}
@@ -51,7 +51,7 @@ kvPairList: kvPair (COMMA kvPair)* COMMA?;
// SELECT statement
select: (selectSetStmt | selectStmt | tSQLxTagElement) SEMICOLON? EOF;
select: (selectSetStmt | selectStmt | tRQLxTagElement) SEMICOLON? EOF;
selectStmtWithParens: selectStmt | LPAREN selectSetStmt RPAREN | placeholder;
@@ -161,7 +161,7 @@ columnExpr
| identifier (LPAREN columnExprs=columnExprList? RPAREN)? LPAREN DISTINCT? columnArgList=columnExprList? RPAREN # ColumnExprFunction
| columnExpr LPAREN selectSetStmt RPAREN # ColumnExprCallSelect
| columnExpr LPAREN columnExprList? RPAREN # ColumnExprCall
| tSQLxTagElement # ColumnExprTagElement
| tRQLxTagElement # ColumnExprTagElement
| templateString # ColumnExprTemplateString
| literal # ColumnExprLiteral
@@ -224,16 +224,16 @@ columnLambdaExpr:
ARROW (columnExpr | block)
;
tSQLxChildElement
: tSQLxTagElement
| TSQLX_TEXT_TEXT
tRQLxChildElement
: tRQLxTagElement
| TRQLX_TEXT_TEXT
| LBRACE columnExpr RBRACE;
tSQLxTagElement
: LT identifier tSQLxTagAttribute* SLASH_GT
| LT identifier tSQLxTagAttribute* GT tSQLxChildElement* LT_SLASH identifier GT
tRQLxTagElement
: LT identifier tRQLxTagAttribute* SLASH_GT
| LT identifier tRQLxTagAttribute* GT tRQLxChildElement* LT_SLASH identifier GT
;
tSQLxTagAttribute
tRQLxTagAttribute
: identifier EQ_SINGLE string
| identifier EQ_SINGLE LBRACE columnExpr RBRACE
| identifier
@@ -247,8 +247,8 @@ withExpr
;
// This is slightly different in TSQL compared to ClickHouse SQL
// TSQL allows unlimited ("*") nestedIdentifier-s "properties.b.a.a.w.a.s".
// This is slightly different in TRQL compared to ClickHouse SQL
// TRQL allows unlimited ("*") nestedIdentifier-s "properties.b.a.a.w.a.s".
// We parse and convert "databaseIdentifier.tableIdentifier.columnIdentifier.nestedIdentifier.*"
// to just one ast.Field(chain=['a','b','columnIdentifier','on','and','on']).
columnIdentifier: placeholder | ((tableIdentifier DOT)? nestedIdentifier);
@@ -258,7 +258,7 @@ tableExpr
| tableFunctionExpr # TableExprFunction
| LPAREN selectSetStmt RPAREN # TableExprSubquery
| tableExpr (alias | AS identifier) # TableExprAlias
| tSQLxTagElement # TableExprTag
| tRQLxTagElement # TableExprTag
| placeholder # TableExprPlaceholder
;
tableFunctionExpr: identifier LPAREN tableArgList? RPAREN;
@@ -337,8 +337,8 @@ FULL_STRING_TEXT
FULL_STRING_ESCAPE_TRIGGER
TAG_WS
TAGC_WS
TSQLX_TEXT_TEXT
TSQLX_TEXT_WS
TRQLX_TEXT_TEXT
TRQLX_TEXT_WS
rule names:
program
@@ -404,9 +404,9 @@ columnTypeExpr
columnExprList
columnExpr
columnLambdaExpr
tSQLxChildElement
tSQLxTagElement
tSQLxTagAttribute
tRQLxChildElement
tRQLxTagElement
tRQLxTagAttribute
withExprList
withExpr
columnIdentifier
@@ -164,8 +164,8 @@ FULL_STRING_TEXT=163
FULL_STRING_ESCAPE_TRIGGER=164
TAG_WS=165
TAGC_WS=166
TSQLX_TEXT_TEXT=167
TSQLX_TEXT_WS=168
TRQLX_TEXT_TEXT=167
TRQLX_TEXT_WS=168
'->'=114
'*'=115
'`'=116
@@ -1,13 +1,13 @@
import { CharStreams, CommonTokenStream } from "antlr4ts";
import { TSQLLexer } from "./TSQLLexer.js";
import { TSQLParser } from "./TSQLParser.js";
import { TRQLLexer } from "./TRQLLexer";
import { TRQLParser } from "./TRQLParser";
describe("TSQLParser", () => {
describe("TRQLParser", () => {
function parse(input: string) {
const inputStream = CharStreams.fromString(input);
const lexer = new TSQLLexer(inputStream);
const lexer = new TRQLLexer(inputStream);
const tokenStream = new CommonTokenStream(lexer);
const parser = new TSQLParser(tokenStream);
const parser = new TRQLParser(tokenStream);
return parser;
}
@@ -17,7 +17,7 @@ describe("TSQLParser", () => {
const tree = parser.select();
expect(tree).toBeDefined();
// The select rule can return selectStmt, selectSetStmt, or tSQLxTagElement
// The select rule can return selectStmt, selectSetStmt, or tRQLxTagElement
// Most SELECT statements are wrapped in selectSetStmt
const selectSetStmt = tree.selectSetStmt();
expect(selectSetStmt).toBeDefined();
@@ -1,12 +1,12 @@
// TSQL - Type-Safe SQL Query Language for ClickHouse
// TRQL - Type-Safe SQL Query Language for ClickHouse
// Originally derived from PostHog's HogQL (see NOTICE.md for attribution)
import { CharStreams, CommonTokenStream } from "antlr4ts";
import type { ANTLRErrorListener, RecognitionException, Recognizer } from "antlr4ts";
import type { Token } from "antlr4ts/Token";
import { TSQLLexer } from "./grammar/TSQLLexer.js";
import { TSQLParser } from "./grammar/TSQLParser.js";
import { TSQLParseTreeConverter } from "./query/parser.js";
import { TRQLLexer } from "./grammar/TRQLLexer";
import { TRQLParser } from "./grammar/TRQLParser";
import { TRQLParseTreeConverter } from "./query/parser.js";
import type { SelectQuery, SelectSetQuery, Expression } from "./query/ast.js";
import { SyntaxError } from "./query/errors.js";
import { createSchemaRegistry, type TableSchema, type FieldMappings } from "./query/schema.js";
@@ -16,7 +16,7 @@ import { printToClickHouse, type PrintResult } from "./query/printer.js";
/**
* Simple error listener that captures syntax errors
*/
class TSQLErrorListener implements ANTLRErrorListener<Token> {
class TRQLErrorListener implements ANTLRErrorListener<Token> {
public error: string | null = null;
syntaxError(
@@ -40,21 +40,21 @@ export * from "./query/errors.js";
// Re-export escape utilities
export {
escapeClickHouseIdentifier,
escapeTSQLIdentifier,
escapeTRQLIdentifier,
escapeClickHouseString,
escapeTSQLString,
escapeTRQLString,
getClickHouseType,
} from "./query/escape.js";
// Re-export function definitions
export {
TSQL_CLICKHOUSE_FUNCTIONS,
TSQL_AGGREGATIONS,
TSQL_COMPARISON_MAPPING,
findTSQLAggregation,
findTSQLFunction,
TRQL_CLICKHOUSE_FUNCTIONS,
TRQL_AGGREGATIONS,
TRQL_COMPARISON_MAPPING,
findTRQLAggregation,
findTRQLFunction,
getAllExposedFunctionNames,
type TSQLFunctionMeta,
type TRQLFunctionMeta,
} from "./query/functions.js";
// Re-export schema types and functions
@@ -104,7 +104,7 @@ export {
export { ClickHousePrinter, printToClickHouse, type PrintResult } from "./query/printer.js";
// Re-export parser converter for advanced usage
export { TSQLParseTreeConverter } from "./query/parser.js";
export { TRQLParseTreeConverter } from "./query/parser.js";
// Re-export validator
export {
@@ -122,26 +122,26 @@ export {
} from "./query/results.js";
/**
* Parse a TSQL SELECT query string into an AST
* Parse a TRQL SELECT query string into an AST
*
* @param query - The TSQL query string to parse
* @param query - The TRQL query string to parse
* @returns The parsed AST (SelectQuery or SelectSetQuery)
* @throws SyntaxError if the query is invalid
*
* @example
* ```typescript
* const ast = parseTSQLSelect("SELECT * FROM users WHERE id = 1");
* const ast = parseTRQLSelect("SELECT * FROM users WHERE id = 1");
* ```
*/
export function parseTSQLSelect(query: string): SelectQuery | SelectSetQuery {
export function parseTRQLSelect(query: string): SelectQuery | SelectSetQuery {
const inputStream = CharStreams.fromString(query);
const lexer = new TSQLLexer(inputStream);
const lexer = new TRQLLexer(inputStream);
const tokenStream = new CommonTokenStream(lexer);
const parser = new TSQLParser(tokenStream);
const parser = new TRQLParser(tokenStream);
// Remove default error listeners and add custom one
parser.removeErrorListeners();
const errorListener = new TSQLErrorListener();
const errorListener = new TRQLErrorListener();
parser.addErrorListener(errorListener);
const parseTree = parser.select();
@@ -150,7 +150,7 @@ export function parseTSQLSelect(query: string): SelectQuery | SelectSetQuery {
throw new SyntaxError(errorListener.error);
}
const converter = new TSQLParseTreeConverter();
const converter = new TRQLParseTreeConverter();
const ast = converter.visit(parseTree);
// Validate the result is a select query
@@ -166,26 +166,26 @@ export function parseTSQLSelect(query: string): SelectQuery | SelectSetQuery {
}
/**
* Parse a TSQL expression string into an AST
* Parse a TRQL expression string into an AST
*
* @param expr - The TSQL expression string to parse
* @param expr - The TRQL expression string to parse
* @returns The parsed expression AST
* @throws SyntaxError if the expression is invalid
*
* @example
* ```typescript
* const ast = parseTSQLExpr("id = 1 AND name = 'test'");
* const ast = parseTRQLExpr("id = 1 AND name = 'test'");
* ```
*/
export function parseTSQLExpr(expr: string): Expression {
export function parseTRQLExpr(expr: string): Expression {
const inputStream = CharStreams.fromString(expr);
const lexer = new TSQLLexer(inputStream);
const lexer = new TRQLLexer(inputStream);
const tokenStream = new CommonTokenStream(lexer);
const parser = new TSQLParser(tokenStream);
const parser = new TRQLParser(tokenStream);
// Remove default error listeners and add custom one
parser.removeErrorListeners();
const errorListener = new TSQLErrorListener();
const errorListener = new TRQLErrorListener();
parser.addErrorListener(errorListener);
const parseTree = parser.columnExpr(0);
@@ -194,14 +194,14 @@ export function parseTSQLExpr(expr: string): Expression {
throw new SyntaxError(errorListener.error);
}
const converter = new TSQLParseTreeConverter();
const converter = new TRQLParseTreeConverter();
return converter.visit(parseTree) as Expression;
}
/**
* Options for compiling a TSQL query to ClickHouse SQL
* Options for compiling a TRQL query to ClickHouse SQL
*/
export interface CompileTSQLOptions {
export interface CompileTRQLOptions {
/** The organization ID for tenant isolation (required) */
organizationId: string;
/** The project ID for tenant isolation (optional - omit to query across all projects) */
@@ -227,15 +227,15 @@ export interface CompileTSQLOptions {
}
/**
* Compile a TSQL query string to ClickHouse SQL with parameters
* Compile a TRQL query string to ClickHouse SQL with parameters
*
* This function:
* 1. Parses the TSQL query into an AST
* 1. Parses the TRQL query into an AST
* 2. Validates tables and columns against the schema
* 3. Injects tenant isolation WHERE clauses
* 4. Generates parameterized ClickHouse SQL
*
* @param query - The TSQL query string to compile
* @param query - The TRQL query string to compile
* @param options - Compilation options including tenant IDs and schema
* @returns The compiled SQL and parameters
* @throws SyntaxError if the query is invalid
@@ -243,7 +243,7 @@ export interface CompileTSQLOptions {
*
* @example
* ```typescript
* const { sql, params } = compileTSQL(
* const { sql, params } = compileTRQL(
* "SELECT * FROM task_runs WHERE status = 'completed' LIMIT 100",
* {
* organizationId: "org_123",
@@ -254,9 +254,9 @@ export interface CompileTSQLOptions {
* );
* ```
*/
export function compileTSQL(query: string, options: CompileTSQLOptions): PrintResult {
// 1. Parse the TSQL query
const ast = parseTSQLSelect(query);
export function compileTRQL(query: string, options: CompileTRQLOptions): PrintResult {
// 1. Parse the TRQL query
const ast = parseTRQLSelect(query);
// 2. Create schema registry from table schemas
const schemaRegistry = createSchemaRegistry(options.tableSchema);
@@ -1,6 +1,6 @@
// TypeScript translation of posthog/hogql/ast.py
import type { TSQLContext } from "./context";
import type { TRQLContext } from "./context";
import type {
DatabaseField,
ExpressionField,
@@ -14,7 +14,7 @@ import type {
UnknownDatabaseField,
VirtualTable,
} from "./models";
import type { ConstantDataType, TSQLQuerySettings } from "./constants";
import type { ConstantDataType, TRQLQuerySettings } from "./constants";
// Base types
export interface AST {
@@ -24,10 +24,10 @@ export interface AST {
}
export interface Type extends AST {
get_child?(name: string, context: TSQLContext): Type;
has_child?(name: string, context: TSQLContext): boolean;
resolve_constant_type?(context: TSQLContext): ConstantType;
resolve_column_constant_type?(name: string, context: TSQLContext): ConstantType;
get_child?(name: string, context: TRQLContext): Type;
has_child?(name: string, context: TRQLContext): boolean;
resolve_constant_type?(context: TRQLContext): ConstantType;
resolve_column_constant_type?(name: string, context: TRQLContext): ConstantType;
}
export interface Expr extends AST {
@@ -75,7 +75,7 @@ export type Expression =
| SelectSetQuery
| RatioExpr
| SampleExpr
| TSQLXTag;
| TRQLXTag;
export interface CTE extends Expr {
expression_type: "cte";
@@ -97,7 +97,7 @@ export interface FieldAliasType extends Type {
}
export interface BaseTableType extends Type {
resolve_database_table?(context: TSQLContext): Table;
resolve_database_table?(context: TRQLContext): Table;
}
export interface TableType extends BaseTableType {
@@ -503,7 +503,7 @@ export interface JoinExpr extends Expr {
expression_type: "join_expr";
type?: TableOrSelectType;
join_type?: string;
table?: SelectQuery | SelectSetQuery | Placeholder | TSQLXTag | Field;
table?: SelectQuery | SelectSetQuery | Placeholder | TRQLXTag | Field;
table_args?: Expression[];
alias?: string;
table_final?: boolean;
@@ -562,7 +562,7 @@ export interface SelectQuery extends Expr {
limit_by?: LimitByExpr;
limit_with_ties?: boolean;
offset?: Expression;
settings?: TSQLQuerySettings;
settings?: TRQLQuerySettings;
view_name?: string;
}
@@ -602,15 +602,15 @@ export interface SampleExpr extends Expr {
offset_value?: RatioExpr;
}
export interface TSQLXAttribute extends AST {
export interface TRQLXAttribute extends AST {
name: string;
value: any;
}
export interface TSQLXTag extends Expr {
expression_type: "tsqlx_tag";
export interface TRQLXTag extends Expr {
expression_type: "trqlx_tag";
kind: string;
attributes: TSQLXAttribute[];
attributes: TRQLXAttribute[];
// Equivalent to to_dict() method
to_dict?(): Record<string, any>;
}
@@ -44,15 +44,15 @@ export enum LimitContext {
}
// Settings applied at the SELECT level
export interface TSQLQuerySettings {
export interface TRQLQuerySettings {
optimize_aggregation_in_order?: boolean;
date_time_output_format?: string;
date_time_input_format?: string;
join_algorithm?: string;
}
// Settings applied on top of all TSQL queries
export interface TSQLGlobalSettings extends TSQLQuerySettings {
// Settings applied on top of all TRQL queries
export interface TRQLGlobalSettings extends TRQLQuerySettings {
readonly?: number;
max_execution_time?: number;
max_memory_usage?: number;
@@ -3,16 +3,16 @@
import type { LimitContext } from "./constants";
import type { Database } from "./database";
import type { PropertySwapper } from "./property_types";
import type { TSQLTimings } from "./timings";
import type { TRQLTimings } from "./timings";
export interface TSQLNotice {
export interface TRQLNotice {
start?: number;
end?: number;
message: string;
fix?: string;
}
export interface TSQLQueryModifiers {
export interface TRQLQueryModifiers {
optimizeJoinedFilters?: boolean;
debug?: boolean;
timings?: boolean;
@@ -23,7 +23,7 @@ export interface TSQLQueryModifiers {
optimizeProjections?: boolean;
}
export interface TSQLFieldAccess {
export interface TRQLFieldAccess {
input: string[];
type?: "run";
field?: string;
@@ -35,22 +35,22 @@ export interface Team {
project_id: number;
}
export interface TSQLContext {
export interface TRQLContext {
team_id?: number;
team?: Team;
database?: Database;
values: Record<string, any>;
within_non_tsql_query?: boolean;
within_non_trql_query?: boolean;
enable_select_queries?: boolean;
limit_top_select?: boolean;
limit_context?: LimitContext;
output_format?: string | null;
globals?: Record<string, any>;
warnings: TSQLNotice[];
notices: TSQLNotice[];
errors: TSQLNotice[];
timings: TSQLTimings;
modifiers: TSQLQueryModifiers;
warnings: TRQLNotice[];
notices: TRQLNotice[];
errors: TRQLNotice[];
timings: TRQLTimings;
modifiers: TRQLQueryModifiers;
debug?: boolean;
property_swapper?: PropertySwapper;
}
@@ -6,7 +6,7 @@
// Adapt these methods to your database/ORM setup
import type { ConstantType } from "./ast";
import type { TSQLContext, TSQLQueryModifiers, Team } from "./context";
import type { TRQLContext, TRQLQueryModifiers, Team } from "./context";
import type {
DatabaseField,
ExpressionField,
@@ -17,9 +17,9 @@ import type {
TableNode,
VirtualTable,
} from "./models";
import type { TSQLTimings } from "./timings";
import type { TRQLTimings } from "./timings";
import { QueryError, ResolutionError } from "./errors";
import { TSQLTimings as TSQLTimingsClass } from "./timings";
import { TRQLTimings as TRQLTimingsClass } from "./timings";
// Type definitions for schema serialization (adapt to your schema types)
export interface DatabaseSchemaTable {
@@ -73,7 +73,7 @@ export interface DatabaseSchemaEndpointTable extends DatabaseSchemaTable {
export interface DatabaseSchemaField {
name: string;
tsql_value: string;
trql_value: string;
type: DatabaseSerializedFieldType;
schema_valid: boolean;
fields?: string[];
@@ -278,7 +278,7 @@ export class Database {
}
}
serialize(context: TSQLContext, includeOnly?: Set<string>): Record<string, DatabaseSchemaTable> {
serialize(context: TRQLContext, includeOnly?: Set<string>): Record<string, DatabaseSchemaTable> {
// NOTE: This method requires database queries to fetch:
// - DataWarehouseTable objects
// - DataWarehouseSavedQuery objects
@@ -359,8 +359,8 @@ export class Database {
teamId?: number,
options?: {
team?: Team;
modifiers?: TSQLQueryModifiers;
timings?: TSQLTimings;
modifiers?: TRQLQueryModifiers;
timings?: TRQLTimings;
}
): Database {
// NOTE: This method requires extensive database/ORM access:
@@ -373,7 +373,7 @@ export class Database {
//
// This is a skeleton structure - adapt to your setup
const timings = options?.timings || new TSQLTimingsClass();
const timings = options?.timings || new TRQLTimingsClass();
const { team, modifiers } = options || {};
// Validate team/teamId
@@ -405,7 +405,7 @@ export class Database {
// Helper functions
const TSQL_CHARACTERS_TO_BE_WRAPPED = ["@", "-", "!", "$", "+"];
const TRQL_CHARACTERS_TO_BE_WRAPPED = ["@", "-", "!", "$", "+"];
function constantTypeToSerializedFieldType(
constantType: ConstantType
@@ -469,7 +469,7 @@ function constantTypeToSerializedFieldType(
export function serializeFields(
fieldInput: Record<string, FieldOrTable>,
context: TSQLContext,
context: TRQLContext,
tableChain: string[],
dbColumns?: Record<string, any> // DataWarehouseTableColumns
): DatabaseSchemaField[] {
@@ -490,11 +490,11 @@ export function serializeFields(
}
}
let tsqlValue: string;
if (TSQL_CHARACTERS_TO_BE_WRAPPED.some((char) => fieldKey.includes(char))) {
tsqlValue = `\`${fieldKey}\``;
let trqlValue: string;
if (TRQL_CHARACTERS_TO_BE_WRAPPED.some((char) => fieldKey.includes(char))) {
trqlValue = `\`${fieldKey}\``;
} else {
tsqlValue = fieldKey;
trqlValue = fieldKey;
}
if ("hidden" in field && field.hidden) {
@@ -519,7 +519,7 @@ export function serializeFields(
fieldOutput.push({
name: fieldKey,
tsql_value: tsqlValue,
trql_value: trqlValue,
type: fieldType,
schema_valid: schemaValid,
});
@@ -527,13 +527,13 @@ export function serializeFields(
// ExpressionField
const exprField = field as ExpressionField;
// NOTE: Requires resolve_types_from_table
// const resolvedExpr = resolveTypesFromTable(exprField.expr, tableChain, context, 'tsql');
// const resolvedExpr = resolveTypesFromTable(exprField.expr, tableChain, context, 'trql');
// const constantType = resolvedExpr.type?.resolve_constant_type(context);
// const fieldType = constantTypeToSerializedFieldType(constantType) || DatabaseSerializedFieldType.EXPRESSION;
fieldOutput.push({
name: fieldKey,
tsql_value: tsqlValue,
trql_value: trqlValue,
type: DatabaseSerializedFieldType.EXPRESSION,
schema_valid: schemaValid,
});
@@ -549,10 +549,10 @@ export function serializeFields(
fieldOutput.push({
name: fieldKey,
tsql_value: tsqlValue,
trql_value: trqlValue,
type,
schema_valid: schemaValid,
table: resolvedTable.to_printed_tsql ? resolvedTable.to_printed_tsql() : fieldKey,
table: resolvedTable.to_printed_trql ? resolvedTable.to_printed_trql() : fieldKey,
fields: "fields" in resolvedTable ? Object.keys(resolvedTable.fields) : [],
id: "id" in resolvedTable && resolvedTable.id ? String(resolvedTable.id) : fieldKey,
});
@@ -562,10 +562,10 @@ export function serializeFields(
const virtualTable = field as VirtualTable;
fieldOutput.push({
name: fieldKey,
tsql_value: tsqlValue,
trql_value: trqlValue,
type: DatabaseSerializedFieldType.VIRTUAL_TABLE,
schema_valid: schemaValid,
table: virtualTable.to_printed_tsql ? virtualTable.to_printed_tsql() : fieldKey,
table: virtualTable.to_printed_trql ? virtualTable.to_printed_trql() : fieldKey,
fields: Object.keys(virtualTable.fields),
});
} else if ("chain" in field) {
@@ -573,7 +573,7 @@ export function serializeFields(
const traverser = field as FieldTraverser;
fieldOutput.push({
name: fieldKey,
tsql_value: tsqlValue,
trql_value: trqlValue,
type: DatabaseSerializedFieldType.FIELD_TRAVERSER,
schema_valid: schemaValid,
chain: traverser.chain,
@@ -2,7 +2,7 @@
import type { Expr } from "./ast";
export class BaseTSQLError extends Error {
export class BaseTRQLError extends Error {
message: string;
start?: number;
end?: number;
@@ -28,34 +28,34 @@ export class BaseTSQLError extends Error {
}
}
export class ExposedTSQLError extends BaseTSQLError {
export class ExposedTRQLError extends BaseTRQLError {
/** An exception that can be exposed to the user. */
}
export class InternalTSQLError extends BaseTSQLError {
/** An internal exception in the TSQL engine. */
export class InternalTRQLError extends BaseTRQLError {
/** An internal exception in the TRQL engine. */
}
export class SyntaxError extends ExposedTSQLError {
/** The input does not conform to TSQL syntax. */
export class SyntaxError extends ExposedTRQLError {
/** The input does not conform to TRQL syntax. */
}
export class QueryError extends ExposedTSQLError {
export class QueryError extends ExposedTRQLError {
/** The query is invalid, though correct syntactically. */
}
export class NotImplementedError extends InternalTSQLError {
/** This feature isn't implemented in TSQL (yet). */
export class NotImplementedError extends InternalTRQLError {
/** This feature isn't implemented in TRQL (yet). */
}
export class ParsingError extends InternalTSQLError {
export class ParsingError extends InternalTRQLError {
/** Parsing failed. */
}
export class ImpossibleASTError extends InternalTSQLError {
export class ImpossibleASTError extends InternalTRQLError {
/** Parsing or resolution resulted in an impossible AST. */
}
export class ResolutionError extends InternalTSQLError {
export class ResolutionError extends InternalTRQLError {
/** Resolution of a table/field/expression failed. */
}
@@ -1,9 +1,9 @@
import { describe, it, expect } from "vitest";
import {
escapeClickHouseIdentifier,
escapeTSQLIdentifier,
escapeTRQLIdentifier,
escapeClickHouseString,
escapeTSQLString,
escapeTRQLString,
getClickHouseType,
SQLValueEscaper,
safeIdentifier,
@@ -43,24 +43,24 @@ describe("escapeClickHouseIdentifier", () => {
});
});
describe("escapeTSQLIdentifier", () => {
describe("escapeTRQLIdentifier", () => {
it("should pass through simple identifiers", () => {
expect(escapeTSQLIdentifier("id")).toBe("id");
expect(escapeTSQLIdentifier("user_name")).toBe("user_name");
expect(escapeTRQLIdentifier("id")).toBe("id");
expect(escapeTRQLIdentifier("user_name")).toBe("user_name");
});
it("should allow dollar signs in identifiers", () => {
expect(escapeTSQLIdentifier("$property")).toBe("$property");
expect(escapeTSQLIdentifier("property$value")).toBe("property$value");
expect(escapeTRQLIdentifier("$property")).toBe("$property");
expect(escapeTRQLIdentifier("property$value")).toBe("property$value");
});
it("should handle numeric identifiers", () => {
expect(escapeTSQLIdentifier(0)).toBe("0");
expect(escapeTSQLIdentifier(123)).toBe("123");
expect(escapeTRQLIdentifier(0)).toBe("0");
expect(escapeTRQLIdentifier(123)).toBe("123");
});
it("should throw for identifiers containing %", () => {
expect(() => escapeTSQLIdentifier("column%name")).toThrow(QueryError);
expect(() => escapeTRQLIdentifier("column%name")).toThrow(QueryError);
});
});
@@ -128,8 +128,8 @@ describe("SQLValueEscaper", () => {
});
});
describe("TSQL dialect", () => {
const escaper = new SQLValueEscaper({ dialect: "tsql" });
describe("TRQL dialect", () => {
const escaper = new SQLValueEscaper({ dialect: "trql" });
it("should escape booleans as keywords", () => {
expect(escaper.visit(true)).toBe("true");
@@ -159,14 +159,14 @@ describe("escapeClickHouseString", () => {
});
});
describe("escapeTSQLString", () => {
describe("escapeTRQLString", () => {
it("should escape string values", () => {
expect(escapeTSQLString("test")).toBe("'test'");
expect(escapeTRQLString("test")).toBe("'test'");
});
it("should handle booleans differently from ClickHouse", () => {
expect(escapeTSQLString(true)).toBe("true");
expect(escapeTSQLString(false)).toBe("false");
expect(escapeTRQLString(true)).toBe("true");
expect(escapeTRQLString(false)).toBe("false");
});
});
@@ -55,22 +55,22 @@ export function escapeParamClickhouse(value: string): string {
}
/**
* Escape an identifier for use in TSQL/HogQL queries
* Escape an identifier for use in TRQL/HogQL queries
* Adapted from clickhouse_driver.util.escape with support for $ in identifiers
*/
export function escapeTSQLIdentifier(identifier: string | number): string {
export function escapeTRQLIdentifier(identifier: string | number): string {
if (typeof identifier === "number") {
// In TSQL we allow integers as identifiers to access array elements
// In TRQL we allow integers as identifiers to access array elements
return String(identifier);
}
if (identifier.includes("%")) {
throw new QueryError(
`The TSQL identifier "${identifier}" is not permitted as it contains the "%" character`
`The TRQL identifier "${identifier}" is not permitted as it contains the "%" character`
);
}
// TSQL allows dollars in the identifier (same regex as frontend escapePropertyAsTSQLIdentifier)
// TRQL allows dollars in the identifier (same regex as frontend escapePropertyAsTRQLIdentifier)
if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(identifier)) {
return identifier;
}
@@ -123,9 +123,9 @@ export type EscapableValue =
*/
export class SQLValueEscaper {
private timezone: string;
private dialect: "tsql" | "clickhouse";
private dialect: "trql" | "clickhouse";
constructor(options: { timezone?: string; dialect?: "tsql" | "clickhouse" } = {}) {
constructor(options: { timezone?: string; dialect?: "trql" | "clickhouse" } = {}) {
this.timezone = options.timezone || "UTC";
this.dialect = options.dialect || "clickhouse";
}
@@ -199,7 +199,7 @@ export class SQLValueEscaper {
const datetimeString = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${ms}000`;
if (this.dialect === "tsql") {
if (this.dialect === "trql") {
return `toDateTime(${this.visitString(datetimeString)})`;
}
return `toDateTime64(${this.visitString(datetimeString)}, 6, ${this.visitString(this.timezone)})`;
@@ -211,10 +211,10 @@ export class SQLValueEscaper {
}
/**
* Escape a value for use in a TSQL/HogQL query string
* Escape a value for use in a TRQL/HogQL query string
*/
export function escapeTSQLString(value: EscapableValue, timezone?: string): string {
return new SQLValueEscaper({ timezone, dialect: "tsql" }).visit(value);
export function escapeTRQLString(value: EscapableValue, timezone?: string): string {
return new SQLValueEscaper({ timezone, dialect: "trql" }).visit(value);
}
/**
@@ -4,9 +4,9 @@
import { CompareOperationOp } from "./ast";
/**
* Metadata for a TSQL function
* Metadata for a TRQL function
*/
export interface TSQLFunctionMeta {
export interface TRQLFunctionMeta {
/** The ClickHouse function name to use */
clickhouseName: string;
/** Minimum number of arguments */
@@ -30,7 +30,7 @@ export interface TSQLFunctionMeta {
/**
* Comparison function mappings from function names to CompareOperationOp
*/
export const TSQL_COMPARISON_MAPPING: Record<string, CompareOperationOp> = {
export const TRQL_COMPARISON_MAPPING: Record<string, CompareOperationOp> = {
equals: CompareOperationOp.Eq,
notEquals: CompareOperationOp.NotEq,
less: CompareOperationOp.Lt,
@@ -46,10 +46,10 @@ export const TSQL_COMPARISON_MAPPING: Record<string, CompareOperationOp> = {
};
/**
* ClickHouse functions available in TSQL
* ClickHouse functions available in TRQL
* Port of HOGQL_CLICKHOUSE_FUNCTIONS from mapping.py
*/
export const TSQL_CLICKHOUSE_FUNCTIONS: Record<string, TSQLFunctionMeta> = {
export const TRQL_CLICKHOUSE_FUNCTIONS: Record<string, TRQLFunctionMeta> = {
// Comparison
equals: { clickhouseName: "equals", minArgs: 2, maxArgs: 2 },
notEquals: { clickhouseName: "notEquals", minArgs: 2, maxArgs: 2 },
@@ -475,10 +475,10 @@ export const TSQL_CLICKHOUSE_FUNCTIONS: Record<string, TSQLFunctionMeta> = {
};
/**
* Aggregate functions available in TSQL
* Aggregate functions available in TRQL
* Port of HOGQL_AGGREGATIONS from aggregations.py
*/
export const TSQL_AGGREGATIONS: Record<string, TSQLFunctionMeta> = {
export const TRQL_AGGREGATIONS: Record<string, TRQLFunctionMeta> = {
// Standard aggregate functions
count: { clickhouseName: "count", minArgs: 0, maxArgs: 1, aggregate: true, caseSensitive: false },
countIf: { clickhouseName: "countIf", minArgs: 1, maxArgs: 2, aggregate: true },
@@ -562,13 +562,13 @@ export const TSQL_AGGREGATIONS: Record<string, TSQLFunctionMeta> = {
};
/**
* Find a function in the TSQL functions map
* Find a function in the TRQL functions map
* Supports case-insensitive lookup for non-case-sensitive functions
*/
function findFunction(
name: string,
functions: Record<string, TSQLFunctionMeta>
): TSQLFunctionMeta | undefined {
functions: Record<string, TRQLFunctionMeta>
): TRQLFunctionMeta | undefined {
const func = functions[name];
if (func !== undefined) {
return func;
@@ -589,25 +589,25 @@ function findFunction(
}
/**
* Find a TSQL aggregation function by name
* Find a TRQL aggregation function by name
*/
export function findTSQLAggregation(name: string): TSQLFunctionMeta | undefined {
return findFunction(name, TSQL_AGGREGATIONS);
export function findTRQLAggregation(name: string): TRQLFunctionMeta | undefined {
return findFunction(name, TRQL_AGGREGATIONS);
}
/**
* Find a TSQL function by name
* Find a TRQL function by name
*/
export function findTSQLFunction(name: string): TSQLFunctionMeta | undefined {
return findFunction(name, TSQL_CLICKHOUSE_FUNCTIONS);
export function findTRQLFunction(name: string): TRQLFunctionMeta | undefined {
return findFunction(name, TRQL_CLICKHOUSE_FUNCTIONS);
}
/**
* Get all exposed function names (for autocomplete, suggestions, etc.)
*/
export function getAllExposedFunctionNames(): string[] {
const functionNames = Object.keys(TSQL_CLICKHOUSE_FUNCTIONS).filter((name) => !name.startsWith("_"));
const aggregationNames = Object.keys(TSQL_AGGREGATIONS).filter((name) => !name.startsWith("_"));
const functionNames = Object.keys(TRQL_CLICKHOUSE_FUNCTIONS).filter((name) => !name.startsWith("_"));
const aggregationNames = Object.keys(TRQL_AGGREGATIONS).filter((name) => !name.startsWith("_"));
return [...functionNames, ...aggregationNames];
}
@@ -1,7 +1,7 @@
// TypeScript translation of posthog/hogql/database/models.py
import type { Expr, ConstantType } from "./ast";
import type { TSQLContext } from "./context";
import type { TRQLContext } from "./context";
export interface FieldOrTable {
hidden?: boolean;
@@ -42,15 +42,15 @@ export interface Table extends FieldOrTable {
fields: Record<string, FieldOrTable>;
has_field?(name: string | number): boolean;
get_field?(name: string | number): FieldOrTable;
to_printed_clickhouse?(context: TSQLContext): string;
to_printed_tsql?(): string;
to_printed_clickhouse?(context: TRQLContext): string;
to_printed_trql?(): string;
avoid_asterisk_fields?(): string[];
get_asterisk?(): Record<string, FieldOrTable>;
}
export interface LazyJoin extends FieldOrTable {
join_function?(from_table: Table, to_table: Table, requesting_table: Table): Expr;
resolve_table?(context: TSQLContext): Table;
resolve_table?(context: TRQLContext): Table;
}
export interface LazyTable extends Table {}
@@ -62,7 +62,7 @@ export interface SavedQuery extends Table {
}
export interface FunctionCallTable extends Table {
call_function?(context: TSQLContext): Expr;
call_function?(context: TRQLContext): Expr;
}
export interface TableNode {
@@ -1,25 +1,25 @@
import { describe, it, expect } from "vitest";
import { CharStreams, CommonTokenStream } from "antlr4ts";
import { TSQLLexer } from "../grammar/TSQLLexer.js";
import { TSQLParser } from "../grammar/TSQLParser.js";
import { TSQLParseTreeConverter } from "./parser.js";
import { TRQLLexer } from "../grammar/TRQLLexer";
import { TRQLParser } from "../grammar/TRQLParser";
import { TRQLParseTreeConverter } from "./parser.js";
import { ArithmeticOperationOp, CompareOperationOp } from "./ast.js";
import { SyntaxError } from "./errors.js";
/**
* Helper function to parse TSQL input and convert to AST
* Helper function to parse TRQL input and convert to AST
*/
function parseAndConvert(input: string) {
const inputStream = CharStreams.fromString(input);
const lexer = new TSQLLexer(inputStream);
const lexer = new TRQLLexer(inputStream);
const tokenStream = new CommonTokenStream(lexer);
const parser = new TSQLParser(tokenStream);
const parser = new TRQLParser(tokenStream);
const parseTree = parser.select();
const converter = new TSQLParseTreeConverter();
const converter = new TRQLParseTreeConverter();
return converter.visit(parseTree);
}
describe("TSQLParseTreeConverter", () => {
describe("TRQLParseTreeConverter", () => {
describe("SELECT statements", () => {
it("should convert a simple SELECT statement", () => {
const ast = parseAndConvert("SELECT * FROM users");
@@ -529,15 +529,15 @@ describe("TSQLParseTreeConverter", () => {
it("should preserve position information in errors", () => {
const input = "SELECT * FROM users WHERE invalid syntax";
const inputStream = CharStreams.fromString(input);
const lexer = new TSQLLexer(inputStream);
const lexer = new TRQLLexer(inputStream);
const tokenStream = new CommonTokenStream(lexer);
const parser = new TSQLParser(tokenStream);
const parser = new TRQLParser(tokenStream);
// This might not parse correctly, but if it does and we visit an error node,
// it should throw with position info
try {
const parseTree = parser.select();
const converter = new TSQLParseTreeConverter();
const converter = new TRQLParseTreeConverter();
converter.visit(parseTree);
} catch (error) {
if (error instanceof SyntaxError) {
@@ -105,9 +105,9 @@ import {
TemplateStringContext,
ThrowStmtContext,
TryCatchStmtContext,
TSQLxChildElementContext,
TSQLxTagAttributeContext,
TSQLxTagElementContext,
TRQLxChildElementContext,
TRQLxTagAttributeContext,
TRQLxTagElementContext,
VarAssignmentContext,
VarDeclContext,
WhereClauseContext,
@@ -121,8 +121,8 @@ import {
WithExprColumnContext,
WithExprListContext,
WithExprSubqueryContext,
} from "../grammar/TSQLParser.js";
import { TSQLParserVisitor } from "../grammar/TSQLParserVisitor.js";
} from "../grammar/TRQLParser";
import { TRQLParserVisitor } from "../grammar/TRQLParserVisitor";
import {
Alias,
And,
@@ -148,8 +148,8 @@ import {
ForInStatement,
ForStatement,
Function,
TSQLXAttribute,
TSQLXTag,
TRQLXAttribute,
TRQLXTag,
IfStatement,
JoinConstraint,
JoinExpr,
@@ -181,7 +181,7 @@ import {
WindowFunction,
} from "./ast";
import { RESERVED_KEYWORDS } from "./constants";
import { BaseTSQLError, NotImplementedError, SyntaxError } from "./errors";
import { BaseTRQLError, NotImplementedError, SyntaxError } from "./errors";
import { parseStringLiteralText } from "./parse_string";
/**
@@ -215,10 +215,10 @@ function getTokenStop(token: Token | undefined): number | undefined {
}
/**
* Visitor that converts TSQL AST to a QueryConfig
* Visitor that converts TRQL AST to a QueryConfig
* The QueryConfig can then be used to build a ClickhouseQueryBuilder
*/
export class TSQLParseTreeConverter implements TSQLParserVisitor<any> {
export class TRQLParseTreeConverter implements TRQLParserVisitor<any> {
start?: number;
constructor(start?: number) {
@@ -339,7 +339,7 @@ export class TSQLParseTreeConverter implements TSQLParserVisitor<any> {
}
return node;
} catch (e: any) {
if (e instanceof BaseTSQLError) {
if (e instanceof BaseTRQLError) {
if (
start !== undefined &&
end !== undefined &&
@@ -570,19 +570,19 @@ export class TSQLParseTreeConverter implements TSQLParserVisitor<any> {
}
// SELECT statements
visitSelect(ctx: SelectContext): SelectQuery | SelectSetQuery | TSQLXTag {
visitSelect(ctx: SelectContext): SelectQuery | SelectSetQuery | TRQLXTag {
const selectSetStmt = ctx.selectSetStmt();
const selectStmt = ctx.selectStmt();
const tSQLxTagElement = ctx.tSQLxTagElement();
const tRQLxTagElement = ctx.tRQLxTagElement();
if (selectSetStmt) {
return this.visitSelectSetStmt(selectSetStmt);
} else if (selectStmt) {
return this.visitSelectStmt(selectStmt);
} else if (tSQLxTagElement) {
return this.visitTsqlxTagElementNested(tSQLxTagElement);
} else if (tRQLxTagElement) {
return this.visitTrqlxTagElementNested(tRQLxTagElement);
}
throw new SyntaxError(
"Select statement must be either a select set statement, a select statement, or a tSQLx tag element"
"Select statement must be either a select set statement, a select statement, or a tRQLx tag element"
);
}
@@ -844,7 +844,7 @@ export class TSQLParseTreeConverter implements TSQLParserVisitor<any> {
return tableResult;
}
// Otherwise, wrap the table expression in a JoinExpr
const table = tableResult as SelectQuery | SelectSetQuery | Placeholder | TSQLXTag | Field;
const table = tableResult as SelectQuery | SelectSetQuery | Placeholder | TRQLXTag | Field;
return {
expression_type: "join_expr",
table,
@@ -856,13 +856,13 @@ export class TSQLParseTreeConverter implements TSQLParserVisitor<any> {
/** Helper for visiting table expressions that may return JoinExpr or table types */
private visitTableExprResult(
ctx: ParserRuleContext
): JoinExpr | SelectQuery | SelectSetQuery | Placeholder | TSQLXTag | Field {
): JoinExpr | SelectQuery | SelectSetQuery | Placeholder | TRQLXTag | Field {
return this.visit(ctx) as
| JoinExpr
| SelectQuery
| SelectSetQuery
| Placeholder
| TSQLXTag
| TRQLXTag
| Field;
}
@@ -1512,7 +1512,7 @@ export class TSQLParseTreeConverter implements TSQLParserVisitor<any> {
return tableResult;
}
// Otherwise, wrap in a JoinExpr
const table = tableResult as SelectQuery | SelectSetQuery | Placeholder | TSQLXTag | Field;
const table = tableResult as SelectQuery | SelectSetQuery | Placeholder | TRQLXTag | Field;
return { expression_type: "join_expr", table, alias };
}
@@ -1520,8 +1520,8 @@ export class TSQLParseTreeConverter implements TSQLParserVisitor<any> {
return this.visitTableFunctionExpr(ctx.tableFunctionExpr());
}
visitTableExprTag(ctx: TableExprTagContext): TSQLXTag {
return this.visitTsqlxTagElementNested(ctx.tSQLxTagElement());
visitTableExprTag(ctx: TableExprTagContext): TRQLXTag {
return this.visitTrqlxTagElementNested(ctx.tRQLxTagElement());
}
visitTableFunctionExpr(ctx: TableFunctionExprContext): JoinExpr {
@@ -1670,51 +1670,51 @@ export class TSQLParseTreeConverter implements TSQLParserVisitor<any> {
};
}
visitTsqlxChildElement(ctx: TSQLxChildElementContext): Expression {
const tSQLxTagElement = ctx.tSQLxTagElement();
if (tSQLxTagElement) {
return this.visitTsqlxTagElementNested(tSQLxTagElement);
visitTrqlxChildElement(ctx: TRQLxChildElementContext): Expression {
const tRQLxTagElement = ctx.tRQLxTagElement();
if (tRQLxTagElement) {
return this.visitTrqlxTagElementNested(tRQLxTagElement);
}
if (ctx.TSQLX_TEXT_TEXT()) {
return this.visitTsqlxText(ctx);
if (ctx.TRQLX_TEXT_TEXT()) {
return this.visitTrqlxText(ctx);
}
return this.visitAsExpr(ctx.columnExpr()!);
}
visitTsqlxText(ctx: TSQLxChildElementContext): Constant {
const text = ctx.TSQLX_TEXT_TEXT();
visitTrqlxText(ctx: TRQLxChildElementContext): Constant {
const text = ctx.TRQLX_TEXT_TEXT();
return { expression_type: "constant", value: text ? text.text : "" };
}
visitTsqlxTagElementClosed(ctx: TSQLxTagElementContext): TSQLXTag {
visitTrqlxTagElementClosed(ctx: TRQLxTagElementContext): TRQLXTag {
const kind = this.visitIdentifier(ctx.identifier()[0]);
const attributes = ctx.tSQLxTagAttribute()
const attributes = ctx.tRQLxTagAttribute()
? ctx
.tSQLxTagAttribute()
.map((a: TSQLxTagAttributeContext) => this.visitTsqlxTagAttribute(a))
.tRQLxTagAttribute()
.map((a: TRQLxTagAttributeContext) => this.visitTrqlxTagAttribute(a))
: [];
return { expression_type: "tsqlx_tag", kind, attributes };
return { expression_type: "trqlx_tag", kind, attributes };
}
visitTsqlxTagElementNested(ctx: TSQLxTagElementContext): TSQLXTag {
visitTrqlxTagElementNested(ctx: TRQLxTagElementContext): TRQLXTag {
const opening = this.visitIdentifier(ctx.identifier(0));
const closing = this.visitIdentifier(ctx.identifier(1));
if (opening !== closing) {
throw new SyntaxError(
`Opening and closing TSQLX tags must match. Got ${opening} and ${closing}`
`Opening and closing TRQLX tags must match. Got ${opening} and ${closing}`
);
}
const attributes = ctx.tSQLxTagAttribute()
const attributes = ctx.tRQLxTagAttribute()
? ctx
.tSQLxTagAttribute()
.map((a: TSQLxTagAttributeContext) => this.visitTsqlxTagAttribute(a))
.tRQLxTagAttribute()
.map((a: TRQLxTagAttributeContext) => this.visitTrqlxTagAttribute(a))
: [];
// ── collect child nodes, discarding pure-indentation whitespace ──
const keptChildren: Expression[] = [];
for (const element of ctx.tSQLxChildElement()) {
const child = this.visitTsqlxChildElement(element);
for (const element of ctx.tRQLxChildElement()) {
const child = this.visitTrqlxChildElement(element);
if ("value" in child && typeof child.value === "string") {
const v = child.value;
@@ -1729,18 +1729,18 @@ export class TSQLParseTreeConverter implements TSQLParserVisitor<any> {
}
if (keptChildren.length > 0) {
if (attributes.some((a: TSQLXAttribute) => a.name === "children")) {
if (attributes.some((a: TRQLXAttribute) => a.name === "children")) {
throw new SyntaxError(
"Can't have a TSQLX tag with both children and a 'children' attribute"
"Can't have a TRQLX tag with both children and a 'children' attribute"
);
}
attributes.push({ name: "children", value: keptChildren });
}
return { expression_type: "tsqlx_tag", kind: opening, attributes };
return { expression_type: "trqlx_tag", kind: opening, attributes };
}
visitTsqlxTagAttribute(ctx: TSQLxTagAttributeContext): TSQLXAttribute {
visitTrqlxTagAttribute(ctx: TRQLxTagAttributeContext): TRQLXAttribute {
const name = this.visitIdentifier(ctx.identifier());
const columnExpr = ctx.columnExpr();
const string = ctx.string();
@@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach } from "vitest";
import { parseTSQLSelect, parseTSQLExpr } from "../index.js";
import { parseTRQLSelect, parseTRQLExpr } from "../index.js";
import { ClickHousePrinter, printToClickHouse, type PrintResult } from "./printer.js";
import { createPrinterContext, PrinterContext } from "./printer_context.js";
import { createSchemaRegistry, column, type TableSchema, type SchemaRegistry } from "./schema.js";
@@ -97,7 +97,7 @@ function createTestContext(
* Helper to print a query and get SQL + params
*/
function printQuery(query: string, context?: PrinterContext) {
const ast = parseTSQLSelect(query);
const ast = parseTRQLSelect(query);
const ctx = context ?? createTestContext();
return printToClickHouse(ast, ctx);
}
@@ -675,14 +675,14 @@ describe("ClickHousePrinter", () => {
it("should throw SyntaxError for malformed queries", () => {
expect(() => {
parseTSQLSelect("SELECT * FORM task_runs"); // typo: FORM instead of FROM
parseTRQLSelect("SELECT * FORM task_runs"); // typo: FORM instead of FROM
}).toThrow();
});
});
describe("Pretty printing", () => {
it("should format SQL with newlines when pretty=true", () => {
const ast = parseTSQLSelect(
const ast = parseTRQLSelect(
"SELECT id, status FROM task_runs WHERE status = 'completed' ORDER BY created_at"
);
const context = createTestContext();
@@ -693,7 +693,7 @@ describe("ClickHousePrinter", () => {
});
it("should produce single-line SQL when pretty=false", () => {
const ast = parseTSQLSelect(
const ast = parseTRQLSelect(
"SELECT id, status FROM task_runs WHERE status = 'completed' ORDER BY created_at"
);
const context = createTestContext();
@@ -722,7 +722,7 @@ describe("ClickHousePrinter", () => {
const { sql, params } = printQuery("SELECT * FROM task_runs WHERE status = 'test'");
// Should have String type in placeholder
expect(sql).toMatch(/\{tsql_val_\d+: String\}/);
expect(sql).toMatch(/\{trql_val_\d+: String\}/);
});
});
});
@@ -1683,7 +1683,7 @@ describe("Field Mapping Value Transformation", () => {
}
function printQuery(query: string, ctx: PrinterContext): PrintResult {
const ast = parseTSQLSelect(query);
const ast = parseTRQLSelect(query);
const printer = new ClickHousePrinter(ctx);
return printer.print(ast);
}
@@ -36,14 +36,14 @@ import {
WindowFrameExpr,
WindowFunction,
} from "./ast";
import { escapeClickHouseIdentifier, escapeTSQLIdentifier, escapeClickHouseString } from "./escape";
import { escapeClickHouseIdentifier, escapeTRQLIdentifier, escapeClickHouseString } from "./escape";
import { ImpossibleASTError, NotImplementedError, QueryError } from "./errors";
import {
TSQL_CLICKHOUSE_FUNCTIONS,
TSQL_AGGREGATIONS,
TSQL_COMPARISON_MAPPING,
findTSQLAggregation,
findTSQLFunction,
TRQL_CLICKHOUSE_FUNCTIONS,
TRQL_AGGREGATIONS,
TRQL_COMPARISON_MAPPING,
findTRQLAggregation,
findTRQLFunction,
validateFunctionArgs,
} from "./functions";
import { PrinterContext } from "./printer_context";
@@ -85,7 +85,7 @@ interface JoinExprResponse {
/**
* ClickHouse SQL Printer
*
* Converts a TSQL AST to a parameterized ClickHouse SQL query with:
* Converts a TRQL AST to a parameterized ClickHouse SQL query with:
* - Automatic tenant isolation (organization_id, project_id, environment_id)
* - Schema-based table/column validation
* - SQL injection protection via parameterized queries
@@ -1025,7 +1025,7 @@ export class ClickHousePrinter {
/**
* Get the virtual column name if a field chain references a virtual column
* @returns The column name (as exposed in TSQL), or null if not a virtual column
* @returns The column name (as exposed in TRQL), or null if not a virtual column
*/
private getVirtualColumnNameForField(chain: Array<string | number>): string | null {
if (chain.length === 0) return null;
@@ -1091,7 +1091,7 @@ export class ClickHousePrinter {
joinStrings.push(tableSchema.clickhouseName);
// Register this table context for column name resolution
// Use the alias if provided, otherwise use the TSQL table name
// Use the alias if provided, otherwise use the TRQL table name
const contextKey = node.alias || tableName;
this.tableContexts.set(contextKey, tableSchema);
@@ -1149,7 +1149,7 @@ export class ClickHousePrinter {
/**
* Create a WHERE clause expression for tenant isolation
* Note: We use just the column name without table prefix since ClickHouse
* requires the actual table name (task_runs_v2), not the TSQL alias (task_runs)
* requires the actual table name (task_runs_v2), not the TRQL alias (task_runs)
*
* Organization ID is always required. Project ID and Environment ID are optional -
* if not provided, the query will return results across all projects/environments.
@@ -1733,8 +1733,8 @@ export class ClickHousePrinter {
const name = node.name;
// Check if this is a comparison function
if (name in TSQL_COMPARISON_MAPPING) {
const op = TSQL_COMPARISON_MAPPING[name];
if (name in TRQL_COMPARISON_MAPPING) {
const op = TRQL_COMPARISON_MAPPING[name];
if (node.args.length !== 2) {
throw new QueryError(`Comparison '${name}' requires exactly two arguments`);
}
@@ -1747,7 +1747,7 @@ export class ClickHousePrinter {
}
// Check for aggregation function
const aggMeta = findTSQLAggregation(name);
const aggMeta = findTRQLAggregation(name);
if (aggMeta) {
validateFunctionArgs(node.args, aggMeta.minArgs, aggMeta.maxArgs, name, {
functionTerm: "aggregation",
@@ -1760,7 +1760,7 @@ export class ClickHousePrinter {
}
if ((stackNode as Call).expression_type === "call" && stackNode !== node) {
const stackCall = stackNode as Call;
if (findTSQLAggregation(stackCall.name)) {
if (findTRQLAggregation(stackCall.name)) {
throw new QueryError(
`Aggregation '${name}' cannot be nested inside another aggregation '${stackCall.name}'`
);
@@ -1776,7 +1776,7 @@ export class ClickHousePrinter {
}
// Check for regular function
const funcMeta = findTSQLFunction(name);
const funcMeta = findTRQLFunction(name);
if (funcMeta) {
validateFunctionArgs(node.args, funcMeta.minArgs, funcMeta.maxArgs, name);
@@ -1904,7 +1904,7 @@ export class ClickHousePrinter {
// ============================================================
/**
* Print a TSQL AST to ClickHouse SQL
* Print a TRQL AST to ClickHouse SQL
*/
export function printToClickHouse(
node: SelectQuery | SelectSetQuery,
@@ -39,7 +39,7 @@ export interface QueryNotice {
}
/**
* Context for the TSQL to ClickHouse printer
* Context for the TRQL to ClickHouse printer
*
* Holds:
* - Tenant IDs for automatic WHERE clause injection
@@ -101,10 +101,10 @@ export class PrinterContext {
* Add a value to the parameter map and return a ClickHouse placeholder
*
* @param value The value to parameterize
* @returns A placeholder string like "{tsql_val_0: String}"
* @returns A placeholder string like "{trql_val_0: String}"
*/
addValue(value: unknown): string {
const key = `tsql_val_${this.paramCounter++}`;
const key = `trql_val_${this.paramCounter++}`;
this.values[key] = value;
const chType = getClickHouseType(value);
return `{${key}: ${chType}}`;
@@ -15,20 +15,20 @@ import type {
CallType,
DateTimeType,
} from "./ast";
import type { TSQLContext } from "./context";
import type { TRQLContext } from "./context";
import type { BooleanDatabaseField, DateTimeDatabaseField, Table } from "./models";
// Helper function to escape TSQL identifiers
function escapeTSQLIdentifier(identifier: string | number): string {
// Helper function to escape TRQL identifiers
function escapeTRQLIdentifier(identifier: string | number): string {
if (typeof identifier === "number") {
return String(identifier);
}
if (identifier.includes("%")) {
throw new Error(
`The TSQL identifier "${identifier}" is not permitted as it contains the "%" character`
`The TRQL identifier "${identifier}" is not permitted as it contains the "%" character`
);
}
// TSQL allows dollars in the identifier
// TRQL allows dollars in the identifier
if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(identifier)) {
return identifier;
}
@@ -69,8 +69,8 @@ function getVisitorMethodName(node: AST): string {
// Handle special cases (matching Python replacements)
const replacements: Record<string, string> = {
tsqlxtag: "tsqlx_tag",
tsqlxattribute: "tsqlx_attribute",
trqlxtag: "trqlx_tag",
trqlxattribute: "trqlx_attribute",
uuidtype: "uuid_type",
string_jsontype: "string_json_type",
};
@@ -263,13 +263,13 @@ class CloningVisitor extends Visitor<any> {
// PropertyFinder: Traverses AST to find all property references
class PropertyFinder extends TraversingVisitor {
context: TSQLContext;
context: TRQLContext;
personProperties: Set<string> = new Set();
eventProperties: Set<string> = new Set();
groupProperties: Map<number, Set<string>> = new Map();
foundTimestamps: boolean = false;
constructor(context: TSQLContext) {
constructor(context: TRQLContext) {
super();
this.context = context;
}
@@ -280,7 +280,7 @@ class PropertyFinder extends TraversingVisitor {
if (this.isBaseTableType(tableType)) {
const table = tableType.resolve_database_table?.(this.context);
if (table) {
const tableName = table.to_printed_tsql?.() || "";
const tableName = table.to_printed_trql?.() || "";
const propertyName = String(node.chain[0]);
if (tableName === "persons" || tableName === "raw_persons") {
@@ -357,7 +357,7 @@ export class PropertySwapper extends CloningVisitor {
eventProperties: Map<string, string>;
personProperties: Map<string, string>;
groupProperties: Map<string, string>;
context: TSQLContext;
context: TRQLContext;
setTimeZones: boolean;
constructor(
@@ -365,7 +365,7 @@ export class PropertySwapper extends CloningVisitor {
eventProperties: Map<string, string> | Record<string, string>,
personProperties: Map<string, string> | Record<string, string>,
groupProperties: Map<string, string> | Record<string, string>,
context: TSQLContext,
context: TRQLContext,
setTimeZones: boolean
) {
super(false); // Don't clear types
@@ -427,7 +427,7 @@ export class PropertySwapper extends CloningVisitor {
} else if (this.isBaseTableType(tableType)) {
const table = tableType.resolve_database_table?.(this.context);
if (table) {
const tableName = table.to_printed_tsql?.() || "";
const tableName = table.to_printed_trql?.() || "";
if (tableName === "persons" || tableName === "raw_persons") {
if (this.personProperties.has(propertyName)) {
@@ -471,7 +471,7 @@ export class PropertySwapper extends CloningVisitor {
if (this.isBaseTableType(tableType)) {
const table = tableType.resolve_database_table?.(this.context);
if (table) {
const tableName = table.to_printed_tsql?.() || "";
const tableName = table.to_printed_trql?.() || "";
if (tableName === "events") {
if (this.personProperties.has(propertyName)) {
return this.convertStringPropertyToType(node, "person", propertyName);
@@ -629,7 +629,7 @@ export class PropertySwapper extends CloningVisitor {
}
// Only highlight the last part of the chain
const lastPart = node.chain[node.chain.length - 1];
const identifierLength = escapeTSQLIdentifier(lastPart).length;
const identifierLength = escapeTRQLIdentifier(lastPart).length;
this.context.notices.push({
start: Math.max(node.start, node.end - identifierLength),
end: node.end,
@@ -671,7 +671,7 @@ export class PropertySwapper extends CloningVisitor {
}
// Main function to build property swapper
export function buildPropertySwapper(node: AST, context: TSQLContext): void {
export function buildPropertySwapper(node: AST, context: TRQLContext): void {
if (!context || !context.team_id) {
return;
}
@@ -1,5 +1,5 @@
/**
* Result transformation utilities for TSQL queries
* Result transformation utilities for TRQL queries
*
* Transforms query result values from internal ClickHouse values
* to user-friendly display names using the column valueMap or fieldMapping.
@@ -100,7 +100,7 @@ function buildColumnTransformMaps(schema: TableSchema[]): Map<string, ColumnSche
const hasFieldMap = hasFieldMapping(columnSchema);
if (hasValueMap || hasFieldMap) {
// Use the TSQL-exposed column name (not the ClickHouse name)
// Use the TRQL-exposed column name (not the ClickHouse name)
columnMaps.set(columnName, columnSchema);
}
}
@@ -1,10 +1,10 @@
// Schema definitions for TSQL query validation
// Schema definitions for TRQL query validation
// Defines allowed tables, columns, and tenant isolation configuration
import { QueryError } from "./errors";
/**
* ClickHouse data types supported by TSQL
* ClickHouse data types supported by TRQL
*/
export type ClickHouseType =
| "String"
@@ -50,7 +50,7 @@ export type ClickHouseType =
* Schema definition for a single column
*/
export interface ColumnSchema {
/** The name of the column as exposed to TSQL queries */
/** The name of the column as exposed to TRQL queries */
name: string;
/** The actual ClickHouse column name (if different from `name`) */
clickhouseName?: string;
@@ -176,7 +176,7 @@ export type FieldMappings = Record<string, Record<string, string>>;
/**
* Metadata for a column in query results.
*
* This is returned by the TSQL compiler to describe each column in the SELECT clause,
* This is returned by the TRQL compiler to describe each column in the SELECT clause,
* allowing the UI to render columns appropriately without inspecting result values.
*/
export interface OutputColumnMetadata {
@@ -213,7 +213,7 @@ export interface TenantColumnConfig {
* Schema definition for a table
*/
export interface TableSchema {
/** The name of the table as exposed to TSQL queries */
/** The name of the table as exposed to TRQL queries */
name: string;
/** The fully qualified ClickHouse table name (e.g., "trigger_dev.task_runs_v2") */
clickhouseName: string;
@@ -1,12 +1,12 @@
/**
* Security Tests for TSQL
* Security Tests for TRQL
*
* These tests verify that the TSQL parser and printer correctly prevent:
* These tests verify that the TRQL parser and printer correctly prevent:
* 1. Cross-tenant data access
* 2. SQL injection attacks
*/
import { describe, expect, it } from "vitest";
import { compileTSQL, type CompileTSQLOptions } from "../index.js";
import { compileTRQL, type CompileTRQLOptions } from "../index.js";
import { column, type TableSchema } from "./schema.js";
/**
@@ -52,15 +52,15 @@ const taskEventsSchema: TableSchema = {
},
};
const defaultOptions: CompileTSQLOptions = {
const defaultOptions: CompileTRQLOptions = {
organizationId: "org_tenant1",
projectId: "proj_tenant1",
environmentId: "env_tenant1",
tableSchema: [taskRunsSchema, taskEventsSchema],
};
function compile(query: string, options: Partial<CompileTSQLOptions> = {}) {
return compileTSQL(query, { ...defaultOptions, ...options });
function compile(query: string, options: Partial<CompileTRQLOptions> = {}) {
return compileTRQL(query, { ...defaultOptions, ...options });
}
describe("Cross-Tenant Security", () => {
@@ -376,7 +376,7 @@ describe("Parameter Safety", () => {
const { sql } = compile("SELECT * FROM task_runs WHERE status = 'test'");
// Parameters should have type annotations like {param: String}
expect(sql).toMatch(/\{tsql_\w+: \w+\}/);
expect(sql).toMatch(/\{trql_\w+: \w+\}/);
});
it("should generate unique parameter names", () => {
@@ -30,8 +30,8 @@ const TIMING_DECIMAL_PLACES = 3; // round to milliseconds
// Not thread safe.
// See trends_query_runner for an example of how to use for multithreaded queries
export class TSQLTimings {
// Completed time in seconds for different parts of the TSQL query
export class TRQLTimings {
// Completed time in seconds for different parts of the TRQL query
timings: Record<string, number> = {};
// Used for housekeeping
@@ -43,8 +43,8 @@ export class TSQLTimings {
this._timingStarts[this._timingPointer] = this.perfCounter();
}
cloneForSubquery(seriesIndex: number): TSQLTimings {
return new TSQLTimings(`${this._timingPointer}/series_${seriesIndex}`);
cloneForSubquery(seriesIndex: number): TRQLTimings {
return new TRQLTimings(`${this._timingPointer}/series_${seriesIndex}`);
}
clearTimings(): void {
@@ -1,6 +1,6 @@
import { describe, it, expect } from "vitest";
import { validateQuery } from "./validator.js";
import { parseTSQLSelect } from "../index.js";
import { parseTRQLSelect } from "../index.js";
import { column, type TableSchema } from "./schema.js";
const runsSchema: TableSchema = {
@@ -25,7 +25,7 @@ const runsSchema: TableSchema = {
};
function validateSQL(query: string, schema: TableSchema[] = [runsSchema]) {
const ast = parseTSQLSelect(query);
const ast = parseTRQLSelect(query);
return validateQuery(ast, schema);
}
@@ -1,4 +1,4 @@
// Schema validation for TSQL queries
// Schema validation for TRQL queries
// Validates column names and enum values against the schema
import type {
@@ -73,7 +73,7 @@ interface ValidationContext {
}
/**
* Validate a parsed TSQL query against a schema
* Validate a parsed TRQL query against a schema
*
* @param ast - The parsed query AST
* @param schema - Array of table schemas to validate against
+5 -5
View File
@@ -303,9 +303,9 @@ importers:
'@internal/tracing':
specifier: workspace:*
version: link:../../internal-packages/tracing
'@internal/tsql':
'@internal/trql':
specifier: workspace:*
version: link:../../internal-packages/tsql
version: link:../../internal-packages/trql
'@internal/zod-worker':
specifier: workspace:*
version: link:../../internal-packages/zod-worker
@@ -1011,9 +1011,9 @@ importers:
'@internal/tracing':
specifier: workspace:*
version: link:../tracing
'@internal/tsql':
'@internal/trql':
specifier: workspace:*
version: link:../tsql
version: link:../trql
'@trigger.dev/core':
specifier: workspace:*
version: link:../../packages/core
@@ -1278,7 +1278,7 @@ importers:
specifier: workspace:*
version: link:../../packages/core
internal-packages/tsql:
internal-packages/trql:
dependencies:
'@trigger.dev/core':
specifier: workspace:*