Compare commits

...

124 Commits

Author SHA1 Message Date
Diego Imbert
7b84b56688 Merge branch 'main' into di/new-duckdb-asset-parser 2025-12-09 17:37:14 +01:00
Diego Imbert
aedc05a830 missing arg 2025-12-09 14:19:36 +01:00
Diego Imbert
f823e7c873 fix transparent assets btn 2025-12-09 14:13:29 +01:00
Diego Imbert
fe2923473d fix duckdb parser detecting too much as asset when RW ctx is unknown 2025-12-09 14:05:29 +01:00
Diego Imbert
a0ac9322fe nits 2025-12-09 13:55:23 +01:00
Diego Imbert
957e37e975 super weird duplication (merge conflict ?) 2025-12-09 10:31:13 +01:00
Diego Imbert
a68f710d83 Merge remote-tracking branch 'origin/main' into di/new-duckdb-asset-parser 2025-12-08 23:59:52 +01:00
Diego Imbert
3489e24ee4 ignore asset parsing errors in frontend (avoid flow layout shift) 2025-12-08 23:59:22 +01:00
Diego Imbert
3a698bfa79 Support default ducklake/datatable main in parser 2025-12-08 23:22:18 +01:00
Diego Imbert
28b3be31ad Add assets as unknown if var was never used 2025-12-08 17:30:09 +01:00
Diego Imbert
f2dc0e69a6 detect implicit access with USE dl; syntax 2025-12-08 17:27:17 +01:00
Diego Imbert
64bf7a40aa detect assets used with dot notation 2025-12-08 17:15:59 +01:00
Diego Imbert
9a5a1ef852 Detect attach + handle returning RW 2025-12-08 16:56:48 +01:00
Diego Imbert
e158c98a7b nit 2025-12-08 16:43:30 +01:00
Diego Imbert
57bfacf1b6 sql asset parser parses most s3 literals 2025-12-08 16:24:34 +01:00
Diego Imbert
699331d158 Merge remote-tracking branch 'origin/main' into di/new-duckdb-asset-parser 2025-12-08 14:51:46 +01:00
Diego Imbert
60c5f40c7d stash 2025-12-05 17:07:00 +01:00
Diego Imbert
e936890a53 Merge branch 'main' into di/data-tables 2025-12-03 22:01:50 +01:00
Diego Imbert
ea8d493bd4 Box pin in test_workflow_as_code to fix stack overflow 2025-12-03 21:53:31 +01:00
Diego Imbert
62c4e735ac sqlx prepare 2025-12-03 20:27:08 +01:00
Diego Imbert
4f604b3fcd Revert "duckdb asset parser var_identifiers"
This reverts commit 88068b1a77.
2025-12-03 16:41:29 +01:00
Diego Imbert
88068b1a77 duckdb asset parser var_identifiers 2025-12-03 15:58:44 +01:00
Diego Imbert
8ae9455278 datatable in duckdb asset parser 2025-12-03 15:32:43 +01:00
Diego Imbert
80270b77f8 DuckDB supports datatables 2025-12-03 15:31:01 +01:00
Diego Imbert
e968cb6b2d Editor bar data tables 2025-12-03 14:57:28 +01:00
Diego Imbert
95340b5647 Ducklake picker 2025-12-03 14:46:37 +01:00
Diego Imbert
5befaa3caa better deeply reactive mutable derived 2025-12-03 14:32:15 +01:00
Diego Imbert
016002f591 stupid mistake 2025-12-03 14:12:37 +01:00
Diego Imbert
79ddaa27ac fix surface hover bug 2025-12-03 14:10:25 +01:00
Diego Imbert
0010df8b1a fork ducklakes and datatables 2025-12-03 13:41:51 +01:00
Diego Imbert
aed78af215 nit missing clone 2025-12-03 13:08:43 +01:00
Diego Imbert
6237d54f10 nit 2025-12-03 12:57:39 +01:00
Diego Imbert
63c5d13b0e ui nit 2025-12-03 11:58:39 +01:00
Diego Imbert
48293f8234 nit fix 2025-12-03 11:51:14 +01:00
Diego Imbert
4b779b6b7e ducklake_user doesnt exist in unit tests 2025-12-03 11:04:51 +01:00
Diego Imbert
634bfd92e1 Merge remote-tracking branch 'origin/main' into di/data-tables 2025-12-03 10:40:42 +01:00
Diego Imbert
5fc5a95c9c ee repo ref 2025-12-03 10:40:29 +01:00
Diego Imbert
4386b52a72 fix frontend ci 2025-12-02 15:34:31 +01:00
Diego Imbert
f4b39866ab manual fix, somehow sqlx prepare won't do it 2025-12-02 15:29:48 +01:00
Diego Imbert
8812016162 Merge remote-tracking branch 'origin/main' into di/data-tables 2025-12-02 14:37:50 +01:00
Diego Imbert
5fb3f034d2 sqlx prepare fail 2025-12-02 14:36:41 +01:00
Diego Imbert
813080f2da nit 2025-12-02 14:18:04 +01:00
Diego Imbert
06aec14333 Go to settings button in Assets Dropdown on not found 2025-12-02 14:11:42 +01:00
Diego Imbert
01bd5ef620 still register ducklake/datatable if not used as unknown R/W 2025-12-02 13:30:53 +01:00
Diego Imbert
fe8dbc2fed Python parser successfully infers R/W/RW from ducklake / datatable 2025-12-02 13:03:09 +01:00
Diego Imbert
da0302ada8 detect data table assigns in var_identifiers 2025-12-02 12:36:25 +01:00
Diego Imbert
53feb9239e remove ducklake/datatable and default 2025-12-02 12:28:09 +01:00
Diego Imbert
7e98d4f08f Revert "claude step 1: detect var declarations"
This reverts commit f866f4819d.
2025-12-02 12:23:45 +01:00
Diego Imbert
cbd634bebc Revert "claude step 2: detect when saved vars are queried"
This reverts commit 1e1f930568.
2025-12-02 12:23:30 +01:00
Diego Imbert
1e1f930568 claude step 2: detect when saved vars are queried 2025-12-02 12:01:51 +01:00
Diego Imbert
20ffb3a2c9 move detect_sql_access_type to common mod 2025-12-02 12:01:21 +01:00
Diego Imbert
f866f4819d claude step 1: detect var declarations 2025-12-02 11:54:27 +01:00
Diego Imbert
f80fdebc65 nit snake case 2025-12-01 17:10:45 +01:00
Diego Imbert
8349aaf3c9 ee repo ref 2025-12-01 17:10:39 +01:00
Diego Imbert
f18085bc9b clearer comment 2025-12-01 16:39:20 +01:00
Diego Imbert
10f7b0095f unnecessary security (admin is required) 2025-12-01 16:34:20 +01:00
Diego Imbert
9cc8edda33 sqlx prepare 2025-12-01 14:35:48 +01:00
Diego Imbert
5ea37534f8 factor out to var_identifiers 2025-12-01 14:30:39 +01:00
Diego Imbert
71d417b9d4 CI fix 2025-12-01 14:18:56 +01:00
Diego Imbert
12991455e3 update const_format 2025-12-01 14:03:33 +01:00
Diego Imbert
9ad0083551 fix merge conflicts 2025-12-01 13:27:23 +01:00
Diego Imbert
0e52c4007d Merge remote-tracking branch 'origin/main' into di/data-tables 2025-12-01 12:52:58 +01:00
Diego Imbert
e62794b64e SqlQuery object in python 2025-12-01 12:46:43 +01:00
Diego Imbert
e957f2ba08 infer_sql_type in python client 2025-12-01 12:23:12 +01:00
Diego Imbert
7cbeb23061 declare module overriding instead of extending 2025-12-01 11:57:19 +01:00
Diego Imbert
a893f9c92a Fix typecheck datatable and ducklake in TS 2025-12-01 11:26:09 +01:00
Diego Imbert
f3dea5da2d typecheck datatable and ducklake name in Typescript 2025-12-01 11:04:22 +01:00
Diego Imbert
5439cd12b3 rename list_custom_instance_pg_databases 2025-12-01 10:58:51 +01:00
Diego Imbert
4003cf0a0e export type SqlTemplateFunction 2025-12-01 10:58:32 +01:00
Diego Imbert
9f10251634 fix sql repl for other read ops than select 2025-11-28 18:55:31 +01:00
Diego Imbert
29de8e3db9 ts asset parser, detect datatable & ducklake R/W 2025-11-28 18:25:11 +01:00
Diego Imbert
152abad35d parse or infer sql types in ts client 2025-11-28 16:43:59 +01:00
Diego Imbert
b65b7cd048 python ducklake client arg fix 2025-11-28 16:32:02 +01:00
Diego Imbert
f7fe83d965 merge conflict fail 2025-11-28 15:56:03 +01:00
Diego Imbert
f5ea5e2a0a Fix migration producing NULL instead of {} when no custom databases 2025-11-28 15:49:12 +01:00
Diego Imbert
76ce9d9d82 nit fix 2025-11-28 15:05:56 +01:00
Diego Imbert
1de0e7dc2c ducklake client python 2025-11-27 18:51:50 +01:00
Diego Imbert
6e70b8dbbb Merge branch 'main' into di/data-tables 2025-11-27 18:42:14 +01:00
Diego Imbert
a58c093c95 datatable and datalake parser in python 2025-11-27 18:04:10 +01:00
Diego Imbert
0af48fcdde basic python datatable client 2025-11-27 17:57:31 +01:00
Diego Imbert
ced21178e8 run_inline_script_preview in python client 2025-11-27 17:38:14 +01:00
Diego Imbert
5d64ad33c1 Fix sed calls in mac 2025-11-27 17:23:38 +01:00
Diego Imbert
711c5ac3da nit 2025-11-27 15:31:33 +01:00
Diego Imbert
e63f39f26f cleaner errors 2025-11-27 15:17:37 +01:00
Diego Imbert
d005e0f1cf nit 2025-11-27 15:00:42 +01:00
Diego Imbert
8e78627e3a asset db icon 2025-11-27 14:56:10 +01:00
Diego Imbert
7cc696421c Data tables display as assets in frontend 2025-11-27 14:50:25 +01:00
Diego Imbert
7b3f3ac4b3 Better typed client 2025-11-27 14:33:38 +01:00
Diego Imbert
58979647ae nit wording 2025-11-27 12:28:29 +01:00
Diego Imbert
1ffccbaf62 run_inline param 2025-11-27 12:27:06 +01:00
Diego Imbert
cc92c15e15 run_language_executor separate fn 2025-11-27 11:53:58 +01:00
Diego Imbert
005867a34f Custom instance setup new look 2025-11-27 08:24:47 +01:00
Diego Imbert
e29882444f nit query -> fetch 2025-11-27 07:52:24 +01:00
Diego Imbert
ff70cfb085 Revert "Migrate S3 permissions to modal"
This reverts commit 0631d03cb0.
2025-11-26 18:58:09 +01:00
Diego Imbert
0631d03cb0 Migrate S3 permissions to modal 2025-11-26 18:43:04 +01:00
Diego Imbert
1dd002e70c Data table assetexplore 2025-11-26 18:18:08 +01:00
Diego Imbert
c42bc39142 fix temp reactivity 2025-11-26 17:36:39 +01:00
Diego Imbert
4013124130 factorize logic 2025-11-26 17:06:24 +01:00
Diego Imbert
fdd3eb3ab3 sqk function for ducklake 2025-11-26 17:01:27 +01:00
Diego Imbert
1f66aeac33 data table instance setup works 2025-11-26 16:47:25 +01:00
Diego Imbert
808bbc7c8f show status in dropdown 2025-11-26 16:41:05 +01:00
Diego Imbert
20409a203e error when trying to use ducklake as datatable or opposite 2025-11-26 16:29:44 +01:00
Diego Imbert
77f84eccd9 add tags to custom instance dbs 2025-11-26 16:13:55 +01:00
Diego Imbert
14fd6c46ea nit wording 2025-11-26 15:18:35 +01:00
Diego Imbert
9c75eebdbb nit 2025-11-26 15:01:20 +01:00
Diego Imbert
69e33f895e Separate custom instance db Select and Wizard components 2025-11-26 14:51:11 +01:00
Diego Imbert
e91f9cdca3 nit 2025-11-26 12:55:24 +01:00
Diego Imbert
fe5c8908d1 separate sqlUtils.ts 2025-11-26 12:53:34 +01:00
Diego Imbert
24bfa4185a println 2025-11-26 10:12:35 +01:00
Diego Imbert
0e6ed2a703 datatables work (without leaking pg credentials) 2025-11-25 19:58:51 +01:00
Diego Imbert
4c7fac7746 Revert "datatables work"
This reverts commit 6e1588d59e.
2025-11-25 19:18:12 +01:00
Diego Imbert
6e1588d59e datatables work 2025-11-25 18:57:54 +01:00
Diego Imbert
bc04f80d19 inline run works 2025-11-25 16:26:22 +01:00
Diego Imbert
aabd194207 ts client basis 2025-11-24 14:22:57 +01:00
Diego Imbert
7bb6c96a5c data tables settings 2025-11-21 23:12:01 +01:00
Diego Imbert
371c2755f5 Safety conversion for old duckdb ffi 2025-11-21 17:46:17 +01:00
Diego Imbert
9ece307d45 fix compilation 2025-11-21 16:45:11 +01:00
Diego Imbert
fddff1b3ca Migrate ducklake catalogs to more generic custom instance databases 2025-11-21 16:20:17 +01:00
Diego Imbert
042d403983 use new toJSONSchema 2025-11-21 13:11:14 +01:00
Diego Imbert
4834b3661e zod 4 fixes 2025-11-21 12:59:27 +01:00
Diego Imbert
6d4f650e11 install runed 2025-11-21 12:41:22 +01:00
Diego Imbert
22fda5b42e Merge remote-tracking branch 'origin/main' into di/data-tables 2025-11-21 12:39:06 +01:00
Diego Imbert
39b93d0172 Merge remote-tracking branch 'origin/main' into di/data-tables 2025-11-14 17:48:46 +01:00
Diego Imbert
efe5bec74a data tables settings ui 2025-11-14 11:27:56 +01:00
17 changed files with 523 additions and 202 deletions

31
backend/Cargo.lock generated
View File

@@ -2822,7 +2822,7 @@ dependencies = [
"parquet",
"rand 0.8.5",
"regex",
"sqlparser",
"sqlparser 0.55.0",
"tempfile",
"tokio",
"url",
@@ -2899,7 +2899,7 @@ dependencies = [
"parquet",
"paste",
"recursive",
"sqlparser",
"sqlparser 0.55.0",
"tokio",
"web-time",
]
@@ -3075,7 +3075,7 @@ dependencies = [
"paste",
"recursive",
"serde_json",
"sqlparser",
"sqlparser 0.55.0",
]
[[package]]
@@ -3371,7 +3371,7 @@ dependencies = [
"log",
"recursive",
"regex",
"sqlparser",
"sqlparser 0.55.0",
]
[[package]]
@@ -8602,15 +8602,6 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "nom"
version = "8.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
dependencies = [
"memchr",
]
[[package]]
name = "notify"
version = "6.1.1"
@@ -12264,6 +12255,17 @@ dependencies = [
"sqlparser_derive",
]
[[package]]
name = "sqlparser"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4591acadbcf52f0af60eafbb2c003232b2b4cd8de5f0e9437cb8b1b59046cc0f"
dependencies = [
"log",
"recursive",
"sqlparser_derive",
]
[[package]]
name = "sqlparser_derive"
version = "0.3.0"
@@ -15184,7 +15186,6 @@ dependencies = [
"lazy_static",
"libloading 0.8.9",
"memchr",
"nom 8.0.0",
"object_store",
"once_cell",
"pep440_rs",
@@ -15705,11 +15706,11 @@ version = "1.590.0"
dependencies = [
"anyhow",
"lazy_static",
"nom 8.0.0",
"regex",
"regex-lite",
"serde",
"serde_json",
"sqlparser 0.59.0",
"windmill-parser",
]

View File

@@ -158,7 +158,6 @@ kube.workspace = true
k8s-openapi.workspace = true
libloading.workspace = true
bitflags.workspace = true
nom.workspace = true
globset.workspace = true
@@ -378,7 +377,6 @@ pg_escape = "0.1.1"
async-nats = "0.38.0"
nkeys = "0.4.4"
nu-parser = { version = "0.101.0", default-features = false }
nom = "8.0.0"
globset = "0.4.16"
process-wrap = { version = "8.2.1", features = ["tokio1"] }

View File

@@ -87,7 +87,7 @@ impl Visitor for AssetsFinder {
fn visit_expr_constant(&mut self, node: ExprConstant) {
match node.value {
Constant::Str(s) => {
if let Some((kind, path)) = parse_asset_syntax(&s) {
if let Some((kind, path)) = parse_asset_syntax(&s, false) {
self.assets.push(ParseAssetsResult {
kind,
path: path.to_string(),
@@ -108,7 +108,7 @@ impl Visitor for AssetsFinder {
if let Expr::Constant(ExprConstant { value: Constant::Str(s), .. }) =
&keyword.value
{
if let Some((kind, path)) = parse_asset_syntax(s) {
if let Some((kind, path)) = parse_asset_syntax(s, false) {
self.assets.push(ParseAssetsResult {
kind,
path: path.to_string(),
@@ -249,7 +249,9 @@ impl AssetsFinder {
match arg_val {
Some(Expr::Constant(ExprConstant { value: Constant::Str(value), .. })) => {
let path = parse_asset_syntax(&value).map(|(_, p)| p).unwrap_or(&value);
let path = parse_asset_syntax(&value, false)
.map(|(_, p)| p)
.unwrap_or(&value);
self.assets
.push(ParseAssetsResult { kind, path: path.to_string(), access_type });
}

View File

@@ -20,4 +20,4 @@ anyhow.workspace = true
lazy_static.workspace = true
serde_json.workspace = true
serde.workspace = true
nom.workspace = true
sqlparser = { version = "0.59.0", features = ["visitor"] }

View File

@@ -1,151 +1,456 @@
use std::collections::HashMap;
use sqlparser::{
ast::{CopyTarget, Expr, ObjectName, TableFactor, Value, ValueWithSpan, Visit, Visitor},
dialect::DuckDbDialect,
parser::Parser,
};
use windmill_parser::asset_parser::{
merge_assets, AssetKind, AssetUsageAccessType, ParseAssetsResult,
merge_assets, parse_asset_syntax, AssetKind, AssetUsageAccessType, ParseAssetsResult,
};
use AssetUsageAccessType::*;
use nom::{
branch::alt,
bytes::complete::{tag, tag_no_case, take_while},
character::complete::{char, multispace0},
combinator::opt,
sequence::preceded,
IResult, Parser,
};
pub fn parse_assets(input: &str) -> anyhow::Result<Vec<ParseAssetsResult<String>>> {
let statements = Parser::parse_sql(&DuckDbDialect, input)?;
pub fn parse_assets<'a>(input: &str) -> anyhow::Result<Vec<ParseAssetsResult<&str>>> {
let mut assets = Vec::new();
let mut remaining = input;
let mut collector = AssetCollector::new();
for statement in statements {
let _ = statement.visit(&mut collector);
}
while !remaining.trim().is_empty() {
if let Ok((rest, _)) = parse_comment(remaining) {
remaining = rest; // skip comment
}
if let Ok((rest, res)) = parse_asset(remaining) {
assets.push(res);
remaining = rest;
} else {
remaining = &remaining[1..]; // skip 1 char and continue
for (_, (kind, path)) in collector.var_identifiers {
if collector
.assets
.iter()
.all(|a| a.path.as_ref() != path || a.kind != kind)
{
collector
.assets
.push(ParseAssetsResult { kind: kind, access_type: None, path: path });
}
}
Ok(merge_assets(assets))
Ok(merge_assets(collector.assets))
}
fn parse_asset(input: &str) -> IResult<&str, ParseAssetsResult<&str>> {
alt((
parse_s3_object_read.map(|path| ParseAssetsResult {
path,
kind: AssetKind::S3Object,
access_type: Some(R),
}),
parse_s3_object_write.map(|path| ParseAssetsResult {
path,
kind: AssetKind::S3Object,
access_type: Some(W),
}),
// Parse ambiguous access_types at the end if we could not find precisely read or copy
parse_s3_object_lit.map(|path| ParseAssetsResult {
path,
kind: AssetKind::S3Object,
access_type: None,
}),
parse_resource_lit.map(|path| ParseAssetsResult {
path,
kind: AssetKind::Resource,
access_type: None,
}),
parse_ducklake_lit.map(|path| ParseAssetsResult {
path,
kind: AssetKind::Ducklake,
access_type: None,
}),
parse_datatable_lit.map(|path| ParseAssetsResult {
path,
kind: AssetKind::DataTable,
access_type: None,
}),
))
.parse(input)
/// Visitor that collects S3 asset literals from SQL statements
struct AssetCollector {
assets: Vec<ParseAssetsResult<String>>,
// e.g set to Read when we are inside a SELECT ... FROM ... statement
current_access_type_stack: Vec<AssetUsageAccessType>,
// e.g ATTACH 'ducklake://a' AS dl; => { "dl": (Ducklake, "a") }
var_identifiers: HashMap<String, (AssetKind, String)>,
// e.g USE dl;
currently_used_asset: Option<(AssetKind, String)>,
}
/// Any expression that reads an s3 asset
fn parse_s3_object_read(input: &str) -> IResult<&str, &str> {
alt((parse_s3_object_read_fn, parse_s3_object_select_from)).parse(input)
impl AssetCollector {
fn new() -> Self {
Self {
assets: Vec::new(),
current_access_type_stack: Vec::with_capacity(8),
var_identifiers: HashMap::new(),
currently_used_asset: None,
}
}
// Detect when we do 'a.b' and 'a' is associated with an asset in var_identifiers
// Or when we access 'b' and we did USE a;
fn get_associated_asset_from_obj_name(
&self,
name: &ObjectName,
) -> Option<ParseAssetsResult<String>> {
let access_type = self.current_access_type_stack.last().copied();
if name.0.len() == 1 {
if name.0.first()?.as_ident()?.quote_style.is_some() {
return None;
}
// We don't want to infer that any simple identifier refers to an asset if
// we are not in a known R/W context
if access_type.is_none() {
return None;
}
if let Some((kind, path)) = &self.currently_used_asset {
return Some(ParseAssetsResult { kind: *kind, access_type, path: path.clone() });
}
}
// Check if the first part of the name (the a in a.b) is associated with an asset
if name.0.len() < 2 {
return None;
}
let ident = name.0.first()?.as_ident()?;
let (kind, path) = self.var_identifiers.get(&ident.value)?;
Some(ParseAssetsResult { kind: *kind, access_type, path: path.clone() })
}
fn handle_string_literal(&mut self, s: &str) {
// Check if the string matches our asset syntax patterns
if let Some((kind, path)) = parse_asset_syntax(s, false) {
if kind == AssetKind::S3Object {
self.assets.push(ParseAssetsResult {
kind,
path: path.to_string(),
access_type: self.current_access_type_stack.last().copied(),
});
}
}
}
fn handle_obj_name_pre(&mut self, name: &ObjectName) {
if let Some(fname) = get_trivial_obj_name(name) {
if is_read_fn(fname) {
self.current_access_type_stack.push(R);
}
}
if let Some(str_lit) = get_str_lit_from_obj_name(name) {
self.handle_string_literal(str_lit);
}
if let Some(asset) = self.get_associated_asset_from_obj_name(name) {
self.assets.push(asset);
}
}
fn handle_obj_name_post(&mut self, name: &ObjectName) {
if self.current_access_type_stack.is_empty() {
return;
}
if let Some(fname) = get_trivial_obj_name(name) {
if is_read_fn(fname) {
self.current_access_type_stack.pop();
}
}
}
}
/// Any expression that writes to an s3 asset
fn parse_s3_object_write(input: &str) -> IResult<&str, &str> {
// COPY (...) TO 's3://...'
let (input, _) = (tag_no_case("TO"), multispace0).parse(input)?;
let (input, path) = parse_s3_object_lit(input)?;
Ok((input, path))
impl Visitor for AssetCollector {
type Break = ();
fn pre_visit_table_factor(
&mut self,
table_factor: &TableFactor,
) -> std::ops::ControlFlow<Self::Break> {
match table_factor {
TableFactor::Table { name, .. } => self.handle_obj_name_pre(name),
_ => {}
}
std::ops::ControlFlow::Continue(())
}
fn post_visit_table_factor(
&mut self,
table_factor: &TableFactor,
) -> std::ops::ControlFlow<Self::Break> {
match table_factor {
TableFactor::Table { name, .. } => self.handle_obj_name_post(name),
_ => {}
}
std::ops::ControlFlow::Continue(())
}
fn pre_visit_expr(&mut self, expr: &Expr) -> std::ops::ControlFlow<Self::Break> {
match expr {
Expr::Value(ValueWithSpan { value: Value::SingleQuotedString(s), .. }) => {
self.handle_string_literal(s)
}
Expr::Value(ValueWithSpan { value: Value::DoubleQuotedString(s), .. }) => {
self.handle_string_literal(s);
}
Expr::Function(func) => self.handle_obj_name_pre(&func.name),
_ => {}
}
std::ops::ControlFlow::Continue(())
}
fn post_visit_expr(&mut self, expr: &Expr) -> std::ops::ControlFlow<Self::Break> {
match expr {
Expr::Function(func) => self.handle_obj_name_post(&func.name),
_ => {}
}
std::ops::ControlFlow::Continue(())
}
fn pre_visit_statement(
&mut self,
statement: &sqlparser::ast::Statement,
) -> std::ops::ControlFlow<Self::Break> {
if let Some(access_type) = get_stmt_access_type(statement) {
self.current_access_type_stack.push(access_type);
}
match statement {
sqlparser::ast::Statement::Copy { target: CopyTarget::File { filename }, .. } => {
self.current_access_type_stack.push(W);
self.handle_string_literal(filename);
self.current_access_type_stack.pop();
}
sqlparser::ast::Statement::AttachDuckDBDatabase {
database_path,
database_alias,
..
} => {
if let Some((kind, path)) = parse_asset_syntax(&database_path.value, true) {
if kind == AssetKind::Ducklake
|| kind == AssetKind::DataTable
|| kind == AssetKind::Resource
{
if let Some(database_alias) = database_alias {
self.var_identifiers
.insert(database_alias.value.clone(), (kind, path.to_string()));
}
}
}
}
sqlparser::ast::Statement::DetachDuckDBDatabase { database_alias, .. } => {
let asset = self.var_identifiers.remove(&database_alias.value);
if self.currently_used_asset == asset {
self.currently_used_asset = None;
}
}
sqlparser::ast::Statement::Use(sqlparser::ast::Use::Object(obj_name)) => {
if let Some((kind, path)) = self.var_identifiers.get(&obj_name.to_string()) {
self.currently_used_asset = Some((*kind, path.clone()));
} else {
self.currently_used_asset = None;
}
}
_ => {}
}
std::ops::ControlFlow::Continue(())
}
fn post_visit_statement(
&mut self,
statement: &sqlparser::ast::Statement,
) -> std::ops::ControlFlow<Self::Break> {
if let Some(_access_type) = get_stmt_access_type(statement) {
self.current_access_type_stack.pop();
}
std::ops::ControlFlow::Continue(())
}
fn pre_visit_query(
&mut self,
_query: &sqlparser::ast::Query,
) -> std::ops::ControlFlow<Self::Break> {
self.current_access_type_stack.push(R);
std::ops::ControlFlow::Continue(())
}
fn post_visit_query(
&mut self,
_query: &sqlparser::ast::Query,
) -> std::ops::ControlFlow<Self::Break> {
self.current_access_type_stack.pop();
std::ops::ControlFlow::Continue(())
}
fn pre_visit_relation(&mut self, relation: &ObjectName) -> std::ops::ControlFlow<Self::Break> {
self.handle_obj_name_pre(relation);
std::ops::ControlFlow::Continue(())
}
fn post_visit_relation(&mut self, relation: &ObjectName) -> std::ops::ControlFlow<Self::Break> {
self.handle_obj_name_post(relation);
std::ops::ControlFlow::Continue(())
}
}
/// read_parquet('s3://...')
fn parse_s3_object_read_fn(input: &str) -> IResult<&str, &str> {
let (input, _) = alt((
tag_no_case("read_parquet"),
tag_no_case("read_csv"),
tag_no_case("read_json"),
))
.parse(input)?;
let (input, _) = multispace0(input)?;
let (input, _) = char('(')(input)?;
let (input, _) = multispace0(input)?;
let (input, path) = parse_s3_object_lit(input)?;
let (input, _) = multispace0(input)?;
let (input, _) = char(')')(input)?;
Ok((input, path))
fn is_read_fn(fname: &str) -> bool {
fname.eq_ignore_ascii_case("read_parquet")
|| fname.eq_ignore_ascii_case("read_csv")
|| fname.eq_ignore_ascii_case("read_json")
}
/// SELECT ... FROM 's3://...'
fn parse_s3_object_select_from(input: &str) -> IResult<&str, &str> {
let (input, _) = tag_no_case("FROM").parse(input)?;
let (input, _) = multispace0(input)?;
let (input, path) = parse_s3_object_lit(input)?;
Ok((input, path))
}
/// 's3://...'
fn parse_s3_object_lit(input: &str) -> IResult<&str, &str> {
let (input, _) = quote(input)?;
let (input, _) = tag("s3://").parse(input)?;
let (input, path) = take_while(|c| c != '\'' && c != '"')(input)?;
let (input, _) = quote(input)?;
Ok((input, path))
fn get_stmt_access_type(statement: &sqlparser::ast::Statement) -> Option<AssetUsageAccessType> {
match statement {
sqlparser::ast::Statement::Query(_) => Some(R),
sqlparser::ast::Statement::Insert(insert) => {
Some(if insert.returning.is_some() { RW } else { W })
}
sqlparser::ast::Statement::Update { returning, .. } => {
Some(if returning.is_some() { RW } else { W })
}
sqlparser::ast::Statement::Delete(delete) => {
Some(if delete.returning.is_some() { RW } else { W })
}
sqlparser::ast::Statement::CreateTable { .. } => Some(W),
sqlparser::ast::Statement::CreateView { .. } => Some(W),
_ => None,
}
}
fn quote(input: &str) -> IResult<&str, char> {
alt((char('\''), char('\"'))).parse(input)
fn get_trivial_obj_name(name: &sqlparser::ast::ObjectName) -> Option<&str> {
if name.0.len() != 1 {
return None;
}
Some(name.0.first()?.as_ident()?.value.as_str())
}
fn parse_resource_lit(input: &str) -> IResult<&str, &str> {
let (input, _) = quote(input)?;
let (input, _) = alt((tag("$res:"), tag("res://"))).parse(input)?;
let (input, path) = take_while(|c| c != '\'' && c != '"')(input)?;
let (input, _) = quote(input)?;
Ok((input, path))
fn get_str_lit_from_obj_name(name: &ObjectName) -> Option<&str> {
if name.0.len() != 1 {
return None;
}
let ident = name.0.first()?.as_ident()?;
if ident.quote_style != Some('\'') {
return None;
}
Some(ident.value.as_str())
}
fn parse_ducklake_lit(input: &str) -> IResult<&str, &str> {
let (input, _) = quote(input)?;
let (input, _) = tag("ducklake").parse(input)?;
let (input, path) =
opt(preceded(tag("://"), take_while(|c| c != '\'' && c != '"'))).parse(input)?;
let (input, _) = quote(input)?;
Ok((input, path.unwrap_or("main")))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sql_asset_parser_s3_literals() {
let input = r#"
SELECT * FROM read_parquet('s3:///a.parquet');
COPY (SELECT * FROM 's3://snd/b.parquet') TO 's3:///c.parquet';
"#;
let s = parse_assets(input);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![
ParseAssetsResult {
kind: AssetKind::S3Object,
path: "/a.parquet".to_string(),
access_type: Some(R)
},
ParseAssetsResult {
kind: AssetKind::S3Object,
path: "/c.parquet".to_string(),
access_type: Some(W)
},
ParseAssetsResult {
kind: AssetKind::S3Object,
path: "snd/b.parquet".to_string(),
access_type: Some(R)
},
])
);
}
fn parse_datatable_lit(input: &str) -> IResult<&str, &str> {
let (input, _) = quote(input)?;
let (input, _) = tag("datatable").parse(input)?;
let (input, path) =
opt(preceded(tag("://"), take_while(|c| c != '\'' && c != '"'))).parse(input)?;
let (input, _) = quote(input)?;
Ok((input, path.unwrap_or("main")))
}
#[test]
fn test_sql_asset_parser_attach_no_usage_is_registered_as_unknown() {
let input = r#"
ATTACH 'ducklake://my_dl' AS dl;
SELECT 2;
USE dl;
"#;
let s = parse_assets(input);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::Ducklake,
path: "my_dl".to_string(),
access_type: None
},])
);
}
fn parse_comment(input: &str) -> IResult<&str, &str> {
let (input, _) = tag("--").parse(input)?;
let (input, comment) = take_while(|c| c != '\n')(input)?;
Ok((input, comment))
#[test]
fn test_sql_asset_parser_attach_dot_notation_read() {
let input = r#"
ATTACH 'ducklake://my_dl' AS dl;
SELECT * FROM dl.table1;
"#;
let s = parse_assets(input);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::Ducklake,
path: "my_dl".to_string(),
access_type: Some(R)
},])
);
}
#[test]
fn test_sql_asset_parser_attach_dot_notation_write() {
let input = r#"
ATTACH 'ducklake://my_dl' AS dl;
SELECT dl.read_bait FROM unrelated_table; -- dl. doesn't access the asset
INSERT INTO dl.table1 VALUES ('test');
"#;
let s = parse_assets(input);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::Ducklake,
path: "my_dl".to_string(),
access_type: Some(W)
},])
);
}
#[test]
fn test_sql_asset_parser_detach() {
let input = r#"
ATTACH 'ducklake://my_dl' AS dl;
DETACH dl;
SELECT * FROM dl.table1;
"#;
let s = parse_assets(input);
assert_eq!(s.map_err(|e| e.to_string()), Ok(vec![]));
}
#[test]
fn test_sql_asset_parser_implicit_use_asset() {
let input = r#"
ATTACH 'ducklake://my_dl' AS dl;
USE dl;
INSERT INTO table1 VALUES ('test');
USE memory;
SELECT * FROM table1;
"#;
let s = parse_assets(input);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::Ducklake,
path: "my_dl".to_string(),
access_type: Some(W)
},])
);
}
#[test]
fn test_sql_asset_parser_default_main() {
let input = r#"
ATTACH 'datatable' AS dl;
INSERT INTO dl.table1 VALUES ('test');
"#;
let s = parse_assets(input);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::DataTable,
path: "main".to_string(),
access_type: Some(W)
},])
);
}
#[test]
fn test_sql_asset_parser_create_table() {
let input = r#"
ATTACH 'ducklake' AS dl; USE dl;
CREATE TABLE friends (
name text,
age int
);
INSERT INTO friends VALUES ($name, $age);
SELECT * FROM friends;
"#;
let s = parse_assets(input);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::Ducklake,
path: "main".to_string(),
access_type: Some(RW)
},])
);
}
}

View File

@@ -58,7 +58,7 @@ impl Visit for AssetsFinder {
fn visit_lit(&mut self, node: &swc_ecma_ast::Lit) {
match node {
swc_ecma_ast::Lit::Str(str) => {
if let Some((kind, path)) = parse_asset_syntax(str.value.as_str()) {
if let Some((kind, path)) = parse_asset_syntax(str.value.as_str(), false) {
self.assets.push(ParseAssetsResult {
kind,
path: path.to_string(),
@@ -221,7 +221,9 @@ impl AssetsFinder {
match arg_value.map(|e| e.expr.as_ref()) {
Some(Expr::Lit(Lit::Str(Str { value, .. }))) => {
let path = parse_asset_syntax(&value).map(|(_, p)| p).unwrap_or(&value);
let path = parse_asset_syntax(&value, false)
.map(|(_, p)| p)
.unwrap_or(&value);
self.assets
.push(ParseAssetsResult { kind, path: path.to_string(), access_type });
}

View File

@@ -190,41 +190,36 @@ pub fn parse_ruby(code: &str) -> String {
#[cfg(feature = "sql-parser")]
#[wasm_bindgen]
pub fn parse_assets_sql(code: &str) -> String {
if let Ok(r) = windmill_parser_sql::parse_assets(code) {
return serde_json::to_string(&r).unwrap();
} else {
return "Invalid".to_string();
match windmill_parser_sql::parse_assets(code) {
Ok(r) => serde_json::to_string(&r).unwrap(),
Err(err) => format!("err: {:?}", err),
}
}
#[cfg(feature = "ts-parser")]
#[wasm_bindgen]
pub fn parse_assets_ts(code: &str) -> String {
if let Ok(r) = windmill_parser_ts::parse_assets(code) {
return serde_json::to_string(&r).unwrap();
} else {
return "Invalid".to_string();
match windmill_parser_ts::parse_assets(code) {
Ok(r) => serde_json::to_string(&r).unwrap(),
Err(err) => format!("err: {:?}", err),
}
}
#[cfg(feature = "py-parser")]
#[wasm_bindgen]
pub fn parse_assets_py(code: &str) -> String {
if let Ok(r) = windmill_parser_py::parse_assets(code) {
return serde_json::to_string(&r).unwrap();
} else {
return "Invalid".to_string();
match windmill_parser_py::parse_assets(code) {
Ok(r) => serde_json::to_string(&r).unwrap(),
Err(err) => format!("err: {:?}", err),
}
}
#[cfg(feature = "ansible-parser")]
#[wasm_bindgen]
pub fn parse_assets_ansible(code: &str) -> String {
let o = windmill_parser_yaml::parse_assets(code);
if let Ok(r) = o {
return serde_json::to_string(&r).unwrap();
} else {
return format!("err: {:?}", o.err().unwrap());
match windmill_parser_yaml::parse_assets(code) {
Ok(r) => serde_json::to_string(&r).unwrap(),
Err(err) => format!("err: {:?}", err),
}
}

View File

@@ -1,6 +1,6 @@
use serde::Serialize;
#[derive(Serialize, PartialEq, Clone, Copy)]
#[derive(Serialize, PartialEq, Clone, Copy, Debug)]
#[serde(rename_all(serialize = "lowercase"))]
pub enum AssetUsageAccessType {
R,
@@ -10,7 +10,7 @@ pub enum AssetUsageAccessType {
use AssetUsageAccessType::*;
#[derive(Serialize, PartialEq, Clone, Copy)]
#[derive(Serialize, PartialEq, Clone, Copy, Debug)]
#[serde(rename_all(serialize = "lowercase"))]
pub enum AssetKind {
S3Object,
@@ -19,7 +19,7 @@ pub enum AssetKind {
DataTable,
}
#[derive(Serialize)]
#[derive(Serialize, Debug, PartialEq)]
pub struct ParseAssetsResult<S: AsRef<str>> {
pub kind: AssetKind,
pub path: S,
@@ -58,8 +58,12 @@ pub fn merge_assets<S: AsRef<str>>(assets: Vec<ParseAssetsResult<S>>) -> Vec<Par
arr
}
pub fn parse_asset_syntax(s: &str) -> Option<(AssetKind, &str)> {
if s.starts_with("s3://") {
pub fn parse_asset_syntax(s: &str, enable_default_syntax: bool) -> Option<(AssetKind, &str)> {
if enable_default_syntax && s == "datatable" {
Some((AssetKind::DataTable, "main"))
} else if enable_default_syntax && s == "ducklake" {
Some((AssetKind::Ducklake, "main"))
} else if s.starts_with("s3://") {
Some((AssetKind::S3Object, &s[5..]))
} else if s.starts_with("res://") {
Some((AssetKind::Resource, &s[6..]))

View File

@@ -741,7 +741,7 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS
}
}}
tooltip="Attach a Ducklake to your scripts. Ducklake allows you to manipulate large data on S3 blob files through a traditional SQL interface."
documentationLink="https://www.windmill.dev/docs/core_concepts/ducklake"
documentationLink="https://www.windmill.dev/docs/core_concepts/persistent_storage/ducklake"
itemName="ducklake"
loadItems={async () =>
(await WorkspaceService.listDucklakes({ workspace: $workspaceStore ?? 'NO_W' })).map(
@@ -783,7 +783,7 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS
}
}}
tooltip="Attach a datatable to your script."
documentationLink="https://www.windmill.dev/docs/core_concepts/data_tables"
documentationLink="https://www.windmill.dev/docs/core_concepts/persistent_storage/data_tables"
itemName="data table"
loadItems={async () =>
(await WorkspaceService.listDataTables({ workspace: $workspaceStore ?? 'NO_W' })).map(
@@ -836,7 +836,7 @@ JsonNode ${windmillPathToCamelCaseName(path)} = JsonNode.Parse(await client.GetS
onSelectAndClose={(s3obj) => {
let s = `'${formatS3Object(s3obj)}'`
if (lang === 'duckdb') {
editor?.insertAtCursor(`SELECT * FROM ${s}`)
editor?.insertAtCursor(`SELECT * FROM ${s};`)
} else if (lang === 'python3') {
if (!editor?.getCode().includes('import wmill')) {
editor?.insertAtBeginning('import wmill\n')

View File

@@ -58,6 +58,7 @@
import { copilotInfo } from '$lib/aiStore'
import JsonInputs from '$lib/components/JsonInputs.svelte'
import Toggle from './Toggle.svelte'
import { deepEqual } from 'fast-equals'
interface Props {
// Exported
@@ -158,12 +159,14 @@
$effect(() => {
;[lang, code]
untrack(() => {
inferAssets(lang, code).then((newAssets: AssetWithAltAccessType[]) => {
inferAssets(lang, code).then((inferAssetsResult) => {
if (inferAssetsResult.status === 'error') return
let newAssets = inferAssetsResult.assets as AssetWithAltAccessType[]
for (const asset of newAssets) {
const old = assets?.find((a) => assetEq(a, asset))
if (old?.alt_access_type) asset.alt_access_type = old.alt_access_type
}
assets = newAssets
if (!deepEqual(assets, newAssets)) assets = newAssets
})
if (lang === 'ansible') {

View File

@@ -107,7 +107,7 @@
'text-xs flex items-center gap-1.5 px-2 rounded-md relative',
'border',
'bg-surface hover:bg-surface-hover active:bg-surface',
'transition-all hover:text-primary cursor-pointer'
'transition-all hover:text-primary backdrop-blur-md cursor-pointer'
)}
>
<div
@@ -146,9 +146,7 @@
<div
class={twMerge(
'text-xs font-normal border text-primary w-10 p-1 text-center rounded-md',
!asset.access_type && !asset.alt_access_type
? 'text-orange-500 !border-orange-500'
: '',
!asset.access_type ? 'text-orange-600 !border-orange-600' : '',
!asset.access_type ? 'hover:bg-surface active:opacity-80' : ''
)}
>
@@ -162,7 +160,11 @@
<span class="text-xs">Please select manually </span>
</span>
<div class="flex items-center gap-2">
<ToggleButtonGroup bind:selected={asset.alt_access_type} class="max-w-fit">
<ToggleButtonGroup
allowEmpty
bind:selected={asset.alt_access_type}
class="max-w-fit"
>
{#snippet children({ item })}
<ToggleButton value="r" label="Read" {item} />
<ToggleButton value="w" label="Write" {item} />

View File

@@ -35,10 +35,9 @@
)
}
if (job.job_kind === 'script') {
return [
...(await inferAssets(job.language!, job.raw_code ?? '')),
...parseInputArgsAssets(job.args ?? {})
]
let inferAssetsResult = await inferAssets(job.language!, job.raw_code ?? '')
let assets = inferAssetsResult.status === 'ok' ? inferAssetsResult.assets : []
return [...assets, ...parseInputArgsAssets(job.args ?? {})]
}
return []
}

View File

@@ -93,8 +93,8 @@ export function formatAssetAccessType(accessType: AssetUsageAccessType | undefin
}
export function getAccessType(asset: AssetWithAltAccessType): AssetUsageAccessType | undefined {
if (asset.alt_access_type) return asset.alt_access_type
if (asset.access_type) return asset.access_type
if (asset.alt_access_type) return asset.alt_access_type
}
export function getFlowModuleAssets(

View File

@@ -119,14 +119,14 @@
})
async function parseAndUpdateRawScriptModule(v: RawScript) {
try {
let parsedAssets: AssetWithAltAccessType[] = await inferAssets(v.language, v.content)
for (const asset of parsedAssets) {
const old = v.assets?.find((a) => assetEq(a, asset))
if (old?.alt_access_type) asset.alt_access_type = old.alt_access_type
}
if (!deepEqual(v.assets, parsedAssets)) v.assets = parsedAssets
} catch (e) {}
let inferAssetsResult = await inferAssets(v.language, v.content)
if (inferAssetsResult.status === 'error') return
let newAssets = inferAssetsResult.assets as AssetWithAltAccessType[]
for (const asset of newAssets) {
const old = v.assets?.find((a) => assetEq(a, asset))
if (old?.alt_access_type) asset.alt_access_type = old.alt_access_type
}
if (!deepEqual(v.assets, newAssets)) v.assets = newAssets
}
// Check for raw script modules whose assets were not parsed. Useful for flows created

View File

@@ -183,7 +183,7 @@
<div class="flex flex-col gap-4 mb-8 mt-20">
<div class="flex flex-col gap-1">
<div class="text-primary text-lg font-semibold">Ducklake</div>
<Description link="https://www.windmill.dev/docs/core_concepts/ducklake">
<Description link="https://www.windmill.dev/docs/core_concepts/persistent_storage/ducklake">
Windmill has first class support for Ducklake. You can use and explore ducklakes like a normal
SQL database, even though the data is actually stored in parquet files in S3 !
</Description>

View File

@@ -89,33 +89,43 @@ async function initWasmRuby() {
await initRubyParser(wasmUrlRuby)
}
type InferAssetsResult =
| { status: 'ok'; assets: AssetWithAccessType[] }
| { status: 'error'; error: string }
export async function inferAssets(
language: SupportedLanguage | undefined,
code: string
): Promise<AssetWithAccessType[]> {
): Promise<InferAssetsResult> {
function wrap(raw_result: string): InferAssetsResult {
if (raw_result.startsWith('err:')) {
return { status: 'error', error: raw_result.slice(4).trim() }
}
return { status: 'ok', assets: JSON.parse(raw_result) as AssetWithAccessType[] }
}
try {
if (language === 'duckdb') {
await initWasmRegex()
let r = JSON.parse(parse_assets_sql(code))
return r
return wrap(parse_assets_sql(code))
}
if (language === 'deno' || language === 'nativets' || language === 'bun') {
await initWasmTs()
return JSON.parse(parse_assets_ts(code))
return wrap(parse_assets_ts(code))
}
if (language === 'python3') {
await initWasmPython()
return JSON.parse(parse_assets_py(code))
return wrap(parse_assets_py(code))
}
if (language === 'ansible') {
await initWasmYaml()
return JSON.parse(parse_assets_ansible(code))
return wrap(parse_assets_ansible(code))
}
} catch (e) {
console.error('error parsing assets', e)
return []
return { status: 'error', error: (e as Error)?.message || JSON.stringify(e) }
}
return []
return { status: 'ok', assets: [] }
}
export async function inferAnsibleExecutionMode(code: string): Promise<any> {

View File

@@ -323,7 +323,7 @@ const DUCKDB_INIT_CODE = `-- result_collection=last_statement_all_rows
-- SELECT * FROM db.public.friends;
-- Click the +Ducklake button to use a ducklake
-- https://www.windmill.dev/docs/core_concepts/ducklake
-- https://www.windmill.dev/docs/core_concepts/persistent_storage/ducklake
--
-- ATTACH 'ducklake' AS dl;
-- USE dl;