rename tslq to trql
This commit is contained in:
+11
-11
@@ -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
|
||||
})
|
||||
);
|
||||
+1
-1
@@ -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";
|
||||
|
||||
+4
-4
@@ -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);
|
||||
+9
-9
@@ -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
|
||||
+26
-26
@@ -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();
|
||||
});
|
||||
});
|
||||
+20
-20
@@ -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";
|
||||
|
||||
+7
-7
@@ -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} />
|
||||
|
||||
+9
-9
@@ -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}
|
||||
@@ -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,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";
|
||||
|
||||
/**
|
||||
|
||||
@@ -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:*",
|
||||
|
||||
+4
-4
@@ -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);
|
||||
+26
-26
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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"
|
||||
|
||||
+29
-29
@@ -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 });
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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?: {
|
||||
|
||||
+45
-45
@@ -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"
|
||||
},
|
||||
+18
-18
@@ -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);
|
||||
+10
-10
File diff suppressed because one or more lines are too long
+2
-2
@@ -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
|
||||
+33
-33
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
lexer grammar TSQLLexer;
|
||||
lexer grammar TRQLLexer;
|
||||
|
||||
@header {
|
||||
// put any global imports you need here
|
||||
+14
-14
@@ -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;
|
||||
+5
-5
@@ -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
|
||||
+2
-2
@@ -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
|
||||
+1697
-1697
File diff suppressed because it is too large
Load Diff
+314
-314
File diff suppressed because it is too large
Load Diff
+6
-6
@@ -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>;
|
||||
}
|
||||
+3
-3
@@ -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;
|
||||
+11
-11
@@ -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;
|
||||
}
|
||||
+22
-22
@@ -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,
|
||||
+12
-12
@@ -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. */
|
||||
}
|
||||
+16
-16
@@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
+11
-11
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
+18
-18
@@ -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];
|
||||
}
|
||||
|
||||
+5
-5
@@ -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 {
|
||||
+11
-11
@@ -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) {
|
||||
+46
-46
@@ -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();
|
||||
+7
-7
@@ -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);
|
||||
}
|
||||
+16
-16
@@ -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,
|
||||
+3
-3
@@ -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}}`;
|
||||
+16
-16
@@ -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;
|
||||
}
|
||||
+2
-2
@@ -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);
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -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;
|
||||
+7
-7
@@ -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", () => {
|
||||
+4
-4
@@ -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 {
|
||||
+2
-2
@@ -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);
|
||||
}
|
||||
|
||||
+2
-2
@@ -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
|
||||
Generated
+5
-5
@@ -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:*
|
||||
|
||||
Reference in New Issue
Block a user