From 152abad35d020175cd9c9df8906e580b4bd4344b Mon Sep 17 00:00:00 2001 From: Diego Imbert Date: Fri, 28 Nov 2025 16:43:59 +0100 Subject: [PATCH] parse or infer sql types in ts client --- typescript-client/sqlUtils.ts | 73 +++++++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 4 deletions(-) diff --git a/typescript-client/sqlUtils.ts b/typescript-client/sqlUtils.ts index 816aa2bfa1..a024992de6 100644 --- a/typescript-client/sqlUtils.ts +++ b/typescript-client/sqlUtils.ts @@ -87,18 +87,34 @@ function sqlProviderImpl( strings: TemplateStringsArray, ...values: any[] ) => { - let formatArg = { + let formatArgDecl = { datatable: (i: number) => `-- $${i + 1} arg${i + 1}`, - ducklake: (i: number) => `-- $arg${i + 1}`, + ducklake: (i: number) => { + let argType = + parseTypeAnnotation(strings[i], strings[i + 1]) || + inferSqlType(values[i]); + return `-- $arg${i + 1} (${argType})`; + }, }[provider]; - let content = values.map((_, i) => formatArg(i)).join("\n") + "\n"; + + let formatArgUsage = { + datatable: (i: number) => { + let argType = + parseTypeAnnotation(strings[i], strings[i + 1]) || + inferSqlType(values[i]); + return `$${i + 1}::${argType}`; + }, + ducklake: (i: number) => `$arg${i + 1}`, + }[provider]; + + let content = values.map((_, i) => formatArgDecl(i)).join("\n") + "\n"; if (provider === "ducklake") content += `ATTACH 'ducklake://${name}' AS dl;USE dl;\n`; let contentBody = ""; for (let i = 0; i < strings.length; i++) { contentBody += strings[i]; - if (i !== strings.length - 1) contentBody += `$${i + 1}`; + if (i !== strings.length - 1) contentBody += formatArgUsage(i); } content += contentBody; @@ -151,3 +167,52 @@ function sqlProviderImpl( }; return sql; } + +// DuckDB executor requires explicit argument types at declaration +// And postgres at argument usage. +// These types exist in both DuckDB and Postgres +// Check that the types exist if you plan to extend this function for other SQL engines. +function inferSqlType(value: any): string { + if (typeof value === "number" || typeof value === "bigint") { + if (Number.isInteger(value)) return "BIGINT"; + return "DOUBLE PRECISION"; + } else if (value === null || value === undefined) { + return "TEXT"; + } else if (typeof value === "string") { + return "TEXT"; + } else if (typeof value === "object") { + return "JSON"; + } else if (typeof value === "boolean") { + return "BOOLEAN"; + } else { + return "TEXT"; + } +} + +// The goal is to detect if the user added a type annotation manually +// +// untyped : sql`SELECT ${x} = 0` => ['SELECT ', ' = 0'] +// typed : sql`SELECT ${x}::int = 0` => ['SELECT ', '::int = 0'] +// typed : sql`SELECT CAST ( ${x} AS int ) = 0` => ['SELECT CAST ( ', ' AS int ) = 0'] +function parseTypeAnnotation( + prevTemplateString: string | undefined, + nextTemplateString: string | undefined +): string | undefined { + if (!nextTemplateString) return; + nextTemplateString = nextTemplateString.trimStart(); + if (nextTemplateString.startsWith("::")) { + return nextTemplateString.substring(2).trimStart().split(/\s+/)[0]; + } + prevTemplateString = prevTemplateString?.trimEnd(); + if ( + prevTemplateString?.endsWith("(") && + prevTemplateString + .substring(0, prevTemplateString.length - 1) + .trim() + .toUpperCase() + .endsWith("CAST") && + nextTemplateString.toUpperCase().startsWith("AS ") + ) { + return nextTemplateString.substring(2).trimStart().split(/\s+/)[0]; + } +}