mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-11 08:07:15 +00:00
feat: sql.raw in Typescript client (#8706)
* feat: detect sql.raw() in TS parser and tag queries with has_raw_interpolation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: filter out sql.raw queries from type-checking and preparation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * feat: implement sql.raw() for inline raw SQL fragments in template literals Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: split sqlProviderImpl into provider interface + shared builder Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix ts client compilation * update asset parser --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
dcd615fdc3
commit
ce290f68db
@@ -106,6 +106,28 @@ fn extract_wmill_datatable_call(expr: &Expr) -> Option<(AssetKind, String, Optio
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Check if an expression is `tag.raw(...)` where `tag` matches the given tag name.
|
||||
/// Returns true for patterns like `sql.raw(x)`.
|
||||
fn is_raw_call(expr: &Expr, tag_name: &str) -> bool {
|
||||
if let Expr::Call(call_expr) = expr {
|
||||
if let Some(Expr::Member(member)) = call_expr.callee.as_expr().map(AsRef::as_ref) {
|
||||
let is_tag = matches!(
|
||||
member.obj.as_ref(),
|
||||
Expr::Ident(ident) if ident.sym.as_str() == tag_name
|
||||
);
|
||||
if is_tag {
|
||||
if let MemberProp::Ident(prop) = &member.prop {
|
||||
return prop.sym.as_str() == "raw";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
const WM_SQL_RAW_PLACEHOLDER: &str = "__WM_SQL_RAW__";
|
||||
|
||||
impl Visit for AssetsFinder {
|
||||
// visit_call_expr will not recurse if it detects an asset,
|
||||
// so this will only be called when no further context was found
|
||||
@@ -230,21 +252,31 @@ impl Visit for AssetsFinder {
|
||||
return;
|
||||
};
|
||||
|
||||
// Extract the SQL query from the template quasis (string parts)
|
||||
// Substitute ${} with $1, $2, etc.
|
||||
let sql: String = node
|
||||
// Determine which interpolations are sql.raw() calls
|
||||
let raw_flags: Vec<bool> = node
|
||||
.tpl
|
||||
.quasis
|
||||
.exprs
|
||||
.iter()
|
||||
.map(|quasi| quasi.raw.as_str())
|
||||
.enumerate()
|
||||
.fold(String::new(), |acc, (i, s)| {
|
||||
if i == 0 {
|
||||
s.to_string()
|
||||
.map(|expr| is_raw_call(expr.as_ref(), tag_name))
|
||||
.collect();
|
||||
let has_raw_interpolation = raw_flags.iter().any(|&r| r);
|
||||
|
||||
// Extract the SQL query from the template quasis (string parts)
|
||||
// Substitute ${} with $N for normal args, __WM_SQL_RAW__ for raw args
|
||||
let mut sql = String::new();
|
||||
let mut arg_index = 0usize;
|
||||
for (i, quasi) in node.tpl.quasis.iter().enumerate() {
|
||||
if i > 0 {
|
||||
let is_raw = raw_flags.get(i - 1).copied().unwrap_or(false);
|
||||
if is_raw {
|
||||
sql.push_str(WM_SQL_RAW_PLACEHOLDER);
|
||||
} else {
|
||||
format!("{}${}{}", acc, i, s)
|
||||
arg_index += 1;
|
||||
sql.push_str(&format!("${}", arg_index));
|
||||
}
|
||||
});
|
||||
}
|
||||
sql.push_str(quasi.raw.as_str());
|
||||
}
|
||||
|
||||
// Capture SQL query details before transforming for SQL parser
|
||||
let span = node.span();
|
||||
@@ -256,6 +288,7 @@ impl Visit for AssetsFinder {
|
||||
source_kind: *kind,
|
||||
source_name: asset_name.clone(),
|
||||
source_schema: schema.clone(),
|
||||
has_raw_interpolation,
|
||||
});
|
||||
|
||||
// We use the SQL parser to detect RW, specific tables, etc.
|
||||
@@ -266,7 +299,11 @@ impl Visit for AssetsFinder {
|
||||
&sql,
|
||||
);
|
||||
match sql_assets {
|
||||
Ok(Some(sql_assets)) => self.assets.extend(sql_assets),
|
||||
Ok(Some(sql_assets)) => self.assets.extend(
|
||||
sql_assets
|
||||
.into_iter()
|
||||
.filter(|a| !a.path.contains(WM_SQL_RAW_PLACEHOLDER)),
|
||||
),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -629,6 +666,7 @@ mod tests {
|
||||
assert_eq!(query_detail.source_kind, AssetKind::DataTable);
|
||||
assert_eq!(query_detail.source_name, "dt");
|
||||
assert_eq!(query_detail.source_schema, None);
|
||||
assert_eq!(query_detail.has_raw_interpolation, false);
|
||||
// Span should be non-zero
|
||||
assert!(query_detail.span.0 > 0);
|
||||
assert!(query_detail.span.1 > query_detail.span.0);
|
||||
@@ -692,5 +730,86 @@ mod tests {
|
||||
assert_eq!(query_detail.source_kind, AssetKind::Ducklake);
|
||||
assert_eq!(query_detail.source_name, "my_lake");
|
||||
assert_eq!(query_detail.source_schema, None);
|
||||
assert_eq!(query_detail.has_raw_interpolation, false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ts_asset_parser_sql_raw_basic() {
|
||||
let input = r#"
|
||||
import * as wmill from "windmill-client"
|
||||
export async function main(table: string) {
|
||||
let sql = wmill.datatable('dt')
|
||||
return await sql`SELECT * FROM ${sql.raw(table)}`.fetch()
|
||||
}
|
||||
"#;
|
||||
let result = parse_assets(input).unwrap();
|
||||
|
||||
// sql.raw in table position => the __WM_SQL_RAW__ asset gets filtered out,
|
||||
// but the datatable itself is still tracked as "used" (without specific table info)
|
||||
assert_eq!(
|
||||
result.assets,
|
||||
vec![ParseAssetsResult {
|
||||
kind: AssetKind::DataTable,
|
||||
path: "dt".to_string(),
|
||||
access_type: None,
|
||||
columns: None,
|
||||
}]
|
||||
);
|
||||
|
||||
// Check SQL query details
|
||||
assert_eq!(result.sql_queries.len(), 1);
|
||||
let q = &result.sql_queries[0];
|
||||
assert!(q.query_string.contains("__WM_SQL_RAW__"));
|
||||
assert!(!q.query_string.contains("$1"));
|
||||
assert_eq!(q.has_raw_interpolation, true);
|
||||
assert_eq!(q.source_kind, AssetKind::DataTable);
|
||||
assert_eq!(q.source_name, "dt");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ts_asset_parser_sql_raw_mixed() {
|
||||
let input = r#"
|
||||
import * as wmill from "windmill-client"
|
||||
export async function main(name: string, col: string, val: number) {
|
||||
let sql = wmill.datatable('dt')
|
||||
return await sql`SELECT * FROM users WHERE name = ${name} AND ${sql.raw(col)} = ${val}`.fetch()
|
||||
}
|
||||
"#;
|
||||
let result = parse_assets(input).unwrap();
|
||||
|
||||
// "users" table should still be detected
|
||||
assert_eq!(result.assets.len(), 1);
|
||||
assert_eq!(result.assets[0].path, "dt/users");
|
||||
assert_eq!(result.assets[0].access_type, Some(R));
|
||||
|
||||
// Check SQL query details — arg numbering skips the raw interpolation
|
||||
assert_eq!(result.sql_queries.len(), 1);
|
||||
let q = &result.sql_queries[0];
|
||||
assert_eq!(
|
||||
q.query_string,
|
||||
"SELECT * FROM users WHERE name = $1 AND __WM_SQL_RAW__ = $2"
|
||||
);
|
||||
assert_eq!(q.has_raw_interpolation, true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ts_asset_parser_sql_raw_no_false_positive() {
|
||||
// Ensure that a normal query (no sql.raw) still has has_raw_interpolation=false
|
||||
let input = r#"
|
||||
import * as wmill from "windmill-client"
|
||||
export async function main(x: number) {
|
||||
let sql = wmill.datatable('dt')
|
||||
return await sql`SELECT * FROM friends WHERE age = ${x}`.fetch()
|
||||
}
|
||||
"#;
|
||||
let result = parse_assets(input).unwrap();
|
||||
assert_eq!(result.sql_queries.len(), 1);
|
||||
assert_eq!(result.sql_queries[0].has_raw_interpolation, false);
|
||||
assert_eq!(
|
||||
result.sql_queries[0].query_string,
|
||||
"SELECT * FROM friends WHERE age = $1"
|
||||
);
|
||||
assert_eq!(result.assets.len(), 1);
|
||||
assert_eq!(result.assets[0].path, "dt/friends");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,8 @@ pub struct SqlQueryDetails {
|
||||
pub source_name: String, // e.g., "main", "dt"
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub source_schema: Option<String>, // e.g., Some("public"), None
|
||||
#[serde(skip_serializing_if = "std::ops::Not::not")]
|
||||
pub has_raw_interpolation: bool, // true if any ${sql.raw(...)} was used
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Default)]
|
||||
|
||||
Generated
+54
-12
@@ -75,8 +75,7 @@
|
||||
"vscode-languageclient": "~9.0.1",
|
||||
"vscode-uri": "~3.1.0",
|
||||
"vscode-ws-jsonrpc": "~3.5.0",
|
||||
"windmill-parser-wasm": "file:../backend/parsers/windmill-parser-wasm/pkg-ts",
|
||||
"windmill-parser-wasm-asset": "1.653.0",
|
||||
"windmill-parser-wasm-asset": "1.673.0",
|
||||
"windmill-parser-wasm-csharp": "1.510.1",
|
||||
"windmill-parser-wasm-go": "1.510.1",
|
||||
"windmill-parser-wasm-java": "1.510.1",
|
||||
@@ -165,7 +164,8 @@
|
||||
},
|
||||
"../backend/parsers/windmill-parser-wasm/pkg-ts": {
|
||||
"name": "windmill-parser-wasm-ts",
|
||||
"version": "1.589.3"
|
||||
"version": "1.673.0",
|
||||
"extraneous": true
|
||||
},
|
||||
"node_modules/@alloc/quick-lru": {
|
||||
"version": "5.2.0",
|
||||
@@ -844,6 +844,7 @@
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz",
|
||||
"integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -855,6 +856,7 @@
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz",
|
||||
"integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -865,6 +867,7 @@
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz",
|
||||
"integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -1354,6 +1357,7 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz",
|
||||
"integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -1510,6 +1514,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1526,6 +1531,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1542,6 +1548,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1558,6 +1565,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1574,6 +1582,7 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1590,6 +1599,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1606,6 +1616,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1622,6 +1633,7 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1638,6 +1650,7 @@
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1654,6 +1667,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1670,6 +1684,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1686,6 +1701,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1702,6 +1718,7 @@
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -1718,6 +1735,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1734,6 +1752,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -2039,6 +2058,7 @@
|
||||
"version": "0.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
||||
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -6847,7 +6867,7 @@
|
||||
"version": "1.21.7",
|
||||
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
|
||||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jiti": "bin/jiti.js"
|
||||
@@ -7346,6 +7366,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7366,6 +7387,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7386,6 +7408,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7406,6 +7429,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7426,6 +7450,7 @@
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7446,6 +7471,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7466,6 +7492,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7486,6 +7513,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7506,6 +7534,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7526,6 +7555,7 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -7546,6 +7576,7 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -12121,6 +12152,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-check/node_modules/picomatch": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/svelte-eslint-parser": {
|
||||
"version": "0.43.0",
|
||||
"resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.43.0.tgz",
|
||||
@@ -12851,7 +12897,7 @@
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@@ -13601,14 +13647,10 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/windmill-parser-wasm": {
|
||||
"resolved": "../backend/parsers/windmill-parser-wasm/pkg-ts",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/windmill-parser-wasm-asset": {
|
||||
"version": "1.653.0",
|
||||
"resolved": "https://registry.npmjs.org/windmill-parser-wasm-asset/-/windmill-parser-wasm-asset-1.653.0.tgz",
|
||||
"integrity": "sha512-Tc9smy79wZSEBxAbiad8D4NiydOVHl541amCRQ93yqqffsmFnyRr4tpBYV/dR5uQ4zdX7aTe5ROwz2jhsXODYQ=="
|
||||
"version": "1.673.0",
|
||||
"resolved": "https://registry.npmjs.org/windmill-parser-wasm-asset/-/windmill-parser-wasm-asset-1.673.0.tgz",
|
||||
"integrity": "sha512-sJZ9YbKhxMT67wWilmT7CDbwX0eVr+rvc6Y4OdHzOXwTwu3MixUinzVyP/lD4WDQW6xzitqOfstHBUrOxpww3A=="
|
||||
},
|
||||
"node_modules/windmill-parser-wasm-csharp": {
|
||||
"version": "1.510.1",
|
||||
|
||||
@@ -148,8 +148,7 @@
|
||||
"vscode-languageclient": "~9.0.1",
|
||||
"vscode-uri": "~3.1.0",
|
||||
"vscode-ws-jsonrpc": "~3.5.0",
|
||||
"windmill-parser-wasm": "file:../backend/parsers/windmill-parser-wasm/pkg-ts",
|
||||
"windmill-parser-wasm-asset": "1.653.0",
|
||||
"windmill-parser-wasm-asset": "1.673.0",
|
||||
"windmill-parser-wasm-csharp": "1.510.1",
|
||||
"windmill-parser-wasm-go": "1.510.1",
|
||||
"windmill-parser-wasm-java": "1.510.1",
|
||||
@@ -593,4 +592,4 @@
|
||||
"@rollup/rollup-linux-x64-gnu": "^4.35.0",
|
||||
"fsevents": "^2.3.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1936,7 +1936,11 @@
|
||||
// The worker will inject type parameters into the code that TypeScript analyzes
|
||||
|
||||
// Worker async function call freezes if we pass a Proxy, $state.snapshot() is very important here
|
||||
updateSqlQueriesInWorker(uri, $state.snapshot(preparedAssetsSqlQueries))
|
||||
// Filter out queries with raw interpolations — they can't be type-checked
|
||||
let queriesToSend = $state
|
||||
.snapshot(preparedAssetsSqlQueries)
|
||||
.filter((q) => !q.has_raw_interpolation)
|
||||
updateSqlQueriesInWorker(uri, queriesToSend)
|
||||
}, 250)
|
||||
|
||||
watch([() => preparedAssetsSqlQueries, () => lang, () => isTsWorkerInitialized.current], () => {
|
||||
|
||||
@@ -31,6 +31,8 @@ export function usePreparedAssetSqlQueries(
|
||||
queries = queries.filter(
|
||||
([_, q]) => q.source_kind === 'datatable' || q.source_kind === 'ducklake'
|
||||
)
|
||||
// Skip queries with raw interpolations — they can't be prepared
|
||||
queries = queries.filter(([_, q]) => !q.has_raw_interpolation)
|
||||
// We only support preparing single-statement queries for now.
|
||||
queries = queries.filter(([_, q]) => getQueryStmtCountHeuristic(q.query_string) === 1)
|
||||
|
||||
|
||||
@@ -187,6 +187,7 @@ export type InferAssetsSqlQueryDetails = {
|
||||
source_kind: 'datatable' | 'ducklake' // AssetKind equivalent
|
||||
source_name: string // e.g., "main", "dt"
|
||||
source_schema?: string // e.g., "public", optional
|
||||
has_raw_interpolation?: boolean // true if any ${sql.raw(...)} was used
|
||||
prepared?: PreparedAssetsSqlQuery
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ def main [
|
||||
|
||||
if ($do_all or $compile) {
|
||||
print "Compiling Typescript..."
|
||||
npx tsdown --format esm --format cjs --no-dts
|
||||
tsc
|
||||
}
|
||||
|
||||
|
||||
Vendored
+7
@@ -71,8 +71,15 @@ export type SqlStatement<T> = {
|
||||
): Promise<void>;
|
||||
};
|
||||
|
||||
export declare class RawSql {
|
||||
readonly __brand: "RawSql";
|
||||
readonly value: string;
|
||||
constructor(value: string);
|
||||
}
|
||||
|
||||
export interface SqlTemplateFunction {
|
||||
<T = any>(strings: TemplateStringsArray, ...values: any[]): SqlStatement<T>;
|
||||
raw(value: string): RawSql;
|
||||
}
|
||||
export interface DatatableSqlTemplateFunction extends SqlTemplateFunction {
|
||||
query<T = any>(sql: string, ...params: any[]): SqlStatement<T>;
|
||||
|
||||
+160
-87
@@ -85,105 +85,137 @@ export type SqlStatement<T> = {
|
||||
): Promise<void>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Wrapper for raw SQL fragments that should be inlined without parameterization.
|
||||
* Created via `sql.raw(value)`.
|
||||
*/
|
||||
export class RawSql {
|
||||
readonly __brand = "RawSql" as const;
|
||||
constructor(public readonly value: string) { }
|
||||
}
|
||||
|
||||
/**
|
||||
* Template tag function for creating SQL statements with parameterized values
|
||||
*/
|
||||
export interface SqlTemplateFunction {
|
||||
<T = any>(strings: TemplateStringsArray, ...values: any[]): SqlStatement<T>;
|
||||
/** Create a raw SQL fragment that will be inlined without parameterization */
|
||||
raw(value: string): RawSql;
|
||||
}
|
||||
|
||||
export interface DatatableSqlTemplateFunction extends SqlTemplateFunction {
|
||||
query<T = any>(sql: string, ...params: any[]): SqlStatement<T>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a SQL template function for PostgreSQL/datatable queries
|
||||
* @param name - Database/datatable name (default: "main")
|
||||
* @returns SQL template function for building parameterized queries
|
||||
* @example
|
||||
* let sql = wmill.datatable()
|
||||
* let name = 'Robin'
|
||||
* let age = 21
|
||||
* await sql`
|
||||
* SELECT * FROM friends
|
||||
* WHERE name = ${name} AND age = ${age}::int
|
||||
* `.fetch()
|
||||
*/
|
||||
export function datatable(name: string = "main"): DatatableSqlTemplateFunction {
|
||||
return sqlProviderImpl(
|
||||
"datatable",
|
||||
parseName(name)
|
||||
) as DatatableSqlTemplateFunction;
|
||||
// ---------------------------------------------------------------------------
|
||||
// Provider interface — captures what differs between datatable and ducklake
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface SqlProvider {
|
||||
formatArgDecl(argNum: number, argType: string): string;
|
||||
formatArgUsage(
|
||||
argNum: number,
|
||||
explicitType: string | undefined,
|
||||
inferredType: string
|
||||
): string;
|
||||
preamble(): string;
|
||||
language: "postgresql" | "duckdb";
|
||||
extraArgs: Record<string, any>;
|
||||
providerName: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a SQL template function for DuckDB/ducklake queries
|
||||
* @param name - DuckDB database name (default: "main")
|
||||
* @returns SQL template function for building parameterized queries
|
||||
* @example
|
||||
* let sql = wmill.ducklake()
|
||||
* let name = 'Robin'
|
||||
* let age = 21
|
||||
* await sql`
|
||||
* SELECT * FROM friends
|
||||
* WHERE name = ${name} AND age = ${age}
|
||||
* `.fetch()
|
||||
*/
|
||||
export function ducklake(name: string = "main"): SqlTemplateFunction {
|
||||
return sqlProviderImpl("ducklake", { name });
|
||||
function datatableProvider(name: string, schema?: string): SqlProvider {
|
||||
return {
|
||||
providerName: "datatable",
|
||||
language: "postgresql",
|
||||
extraArgs: { database: `datatable://${name}` },
|
||||
formatArgDecl: (argNum) => `-- $${argNum} arg${argNum}`,
|
||||
formatArgUsage: (argNum, explicitType, inferredType) =>
|
||||
explicitType !== undefined
|
||||
? `$${argNum}`
|
||||
: `$${argNum}::${inferredType}`,
|
||||
preamble: () => (schema ? `SET search_path TO "${schema}";\n` : ""),
|
||||
};
|
||||
}
|
||||
|
||||
function sqlProviderImpl(
|
||||
provider: "datatable" | "ducklake",
|
||||
{ name, schema }: { name: string; schema?: string }
|
||||
): SqlTemplateFunction {
|
||||
let sqlFn: SqlTemplateFunction = (
|
||||
strings: TemplateStringsArray,
|
||||
...values: any[]
|
||||
) => {
|
||||
let formatArgDecl = {
|
||||
datatable: (i: number) => `-- $${i + 1} arg${i + 1}`,
|
||||
ducklake: (i: number) => {
|
||||
function ducklakeProvider(name: string): SqlProvider {
|
||||
return {
|
||||
providerName: "ducklake",
|
||||
language: "duckdb",
|
||||
extraArgs: {},
|
||||
formatArgDecl: (argNum, argType) => `-- $arg${argNum} (${argType})`,
|
||||
formatArgUsage: (argNum) => `$arg${argNum}`,
|
||||
preamble: () => `ATTACH 'ducklake://${name}' AS dl;USE dl;\n`,
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared template function builder
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function buildSqlTemplateFunction(provider: SqlProvider): SqlTemplateFunction {
|
||||
let sqlFn = ((strings: TemplateStringsArray, ...values: any[]) => {
|
||||
// Separate raw vs parameterized values, assigning arg indices only to params
|
||||
let argIndex = 0;
|
||||
const valueInfos = values.map((v, i) => {
|
||||
if (v instanceof RawSql)
|
||||
return { raw: true as const, value: v.value, originalIndex: i };
|
||||
argIndex++;
|
||||
return {
|
||||
raw: false as const,
|
||||
value: v,
|
||||
originalIndex: i,
|
||||
argNum: argIndex,
|
||||
};
|
||||
});
|
||||
|
||||
// Arg declarations (SQL comments consumed by the executor)
|
||||
let argDecls = valueInfos
|
||||
.filter((info) => !info.raw)
|
||||
.map((info) => {
|
||||
let argType =
|
||||
parseTypeAnnotation(strings[i], strings[i + 1]) ||
|
||||
inferSqlType(values[i]);
|
||||
return `-- $arg${i + 1} (${argType})`;
|
||||
},
|
||||
}[provider];
|
||||
parseTypeAnnotation(
|
||||
strings[info.originalIndex],
|
||||
strings[info.originalIndex + 1]
|
||||
) || inferSqlType(info.value);
|
||||
return provider.formatArgDecl(info.argNum, argType);
|
||||
});
|
||||
|
||||
let formatArgUsage = {
|
||||
datatable: (i: number) => {
|
||||
const parsedType = parseTypeAnnotation(strings[i], strings[i + 1]);
|
||||
if (parsedType !== undefined) return `$${i + 1}`;
|
||||
let argType = 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`;
|
||||
|
||||
if (schema && provider === "datatable") {
|
||||
content += `SET search_path TO "${schema}";\n`;
|
||||
}
|
||||
let content = argDecls.length ? argDecls.join("\n") + "\n" : "";
|
||||
content += provider.preamble();
|
||||
|
||||
// SQL body — inline raw values, reference params via provider syntax
|
||||
let contentBody = "";
|
||||
for (let i = 0; i < strings.length; i++) {
|
||||
contentBody += strings[i];
|
||||
if (i !== strings.length - 1) contentBody += formatArgUsage(i);
|
||||
if (i < valueInfos.length) {
|
||||
let info = valueInfos[i];
|
||||
if (info.raw) {
|
||||
contentBody += info.value;
|
||||
} else {
|
||||
let explicitType = parseTypeAnnotation(
|
||||
strings[info.originalIndex],
|
||||
strings[info.originalIndex + 1]
|
||||
);
|
||||
let inferredType = inferSqlType(info.value);
|
||||
contentBody += provider.formatArgUsage(
|
||||
info.argNum,
|
||||
explicitType,
|
||||
inferredType
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
content += contentBody;
|
||||
|
||||
const args = {
|
||||
...Object.fromEntries(values.map((v, i) => [`arg${i + 1}`, v])),
|
||||
...(provider === "datatable" ? { database: `datatable://${name}` } : {}),
|
||||
...Object.fromEntries(
|
||||
valueInfos
|
||||
.filter((info) => !info.raw)
|
||||
.map((info) => [`arg${info.argNum}`, info.value])
|
||||
),
|
||||
...provider.extraArgs,
|
||||
};
|
||||
const language = {
|
||||
datatable: "postgresql" as const,
|
||||
ducklake: "duckdb" as const,
|
||||
}[provider];
|
||||
|
||||
async function fetch<ResultCollectionT extends ResultCollection>({
|
||||
resultCollection,
|
||||
@@ -193,7 +225,7 @@ function sqlProviderImpl(
|
||||
try {
|
||||
let result = await JobService.runScriptPreviewInline({
|
||||
workspace: getWorkspace(),
|
||||
requestBody: { args, content, language },
|
||||
requestBody: { args, content, language: provider.language },
|
||||
});
|
||||
return result as SqlResult<any, ResultCollectionT>;
|
||||
} catch (e: any) {
|
||||
@@ -207,7 +239,7 @@ function sqlProviderImpl(
|
||||
if (body.startsWith("Internal:")) body = body.slice(9).trim();
|
||||
if (body.startsWith("Error:")) body = body.slice(6).trim();
|
||||
if (body.startsWith("datatable")) body = body.slice(9).trim();
|
||||
err = Error(`${provider} ${body}`);
|
||||
err = Error(`${provider.providerName} ${body}`);
|
||||
err.query = contentBody;
|
||||
err.request = e.request;
|
||||
}
|
||||
@@ -228,21 +260,62 @@ function sqlProviderImpl(
|
||||
}),
|
||||
execute: (params) => fetch(params),
|
||||
} satisfies SqlStatement<any>;
|
||||
};
|
||||
if (provider === "datatable") {
|
||||
(sqlFn as DatatableSqlTemplateFunction).query = (
|
||||
sqlString: string,
|
||||
...params: any[]
|
||||
) => {
|
||||
// This is less than ideal, did that quickly for a client need.
|
||||
// TODO: break down the SqlTemplateFunction impl and reuse here properly.
|
||||
let arr = Object.assign([sqlString], { raw: [sqlString] });
|
||||
return sqlFn(arr, ...params);
|
||||
};
|
||||
}
|
||||
}) as SqlTemplateFunction;
|
||||
|
||||
sqlFn.raw = (value: string) => new RawSql(value);
|
||||
return sqlFn;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Create a SQL template function for PostgreSQL/datatable queries
|
||||
* @param name - Database/datatable name (default: "main")
|
||||
* @returns SQL template function for building parameterized queries
|
||||
* @example
|
||||
* let sql = wmill.datatable()
|
||||
* let name = 'Robin'
|
||||
* let age = 21
|
||||
* await sql`
|
||||
* SELECT * FROM friends
|
||||
* WHERE name = ${name} AND age = ${age}::int
|
||||
* `.fetch()
|
||||
*/
|
||||
export function datatable(name: string = "main"): DatatableSqlTemplateFunction {
|
||||
let { name: n, schema } = parseName(name);
|
||||
let sqlFn = buildSqlTemplateFunction(
|
||||
datatableProvider(n, schema)
|
||||
) as DatatableSqlTemplateFunction;
|
||||
sqlFn.query = (sqlString: string, ...params: any[]) => {
|
||||
let arr = Object.assign([sqlString], { raw: [sqlString] });
|
||||
return sqlFn(arr, ...params);
|
||||
};
|
||||
return sqlFn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a SQL template function for DuckDB/ducklake queries
|
||||
* @param name - DuckDB database name (default: "main")
|
||||
* @returns SQL template function for building parameterized queries
|
||||
* @example
|
||||
* let sql = wmill.ducklake()
|
||||
* let name = 'Robin'
|
||||
* let age = 21
|
||||
* await sql`
|
||||
* SELECT * FROM friends
|
||||
* WHERE name = ${name} AND age = ${age}
|
||||
* `.fetch()
|
||||
*/
|
||||
export function ducklake(name: string = "main"): SqlTemplateFunction {
|
||||
return buildSqlTemplateFunction(ducklakeProvider(name));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Utilities
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// DuckDB executor requires explicit argument types at declaration
|
||||
// And postgres at argument usage.
|
||||
// These types exist in both DuckDB and Postgres
|
||||
|
||||
Reference in New Issue
Block a user