fix: fix DB Manager not working with db resources with 4+ path segments (#7809)

* support more than 3 path segments

* Fix explore db resource not working with 4+ path segments

* don't assume 3 segments

* ?table= syntax impl

* update parsers

* more nit fixes

* fix sql query

* claude nit

* Update SQLx metadata

---------

Co-authored-by: windmill-internal-app[bot] <windmill-internal-app[bot]@users.noreply.github.com>
This commit is contained in:
Diego Imbert
2026-02-05 14:00:15 +00:00
committed by GitHub
co-authored by windmill-internal-app[bot]
parent 6673507824
commit 5cdecd795c
17 changed files with 167 additions and 162 deletions
@@ -15,7 +15,7 @@
]
},
"nullable": [
true
null
]
},
"hash": "5a219a2532517869578c4504ff3153c43903f929ae5d62fbba12610f89c36d55"
@@ -87,7 +87,14 @@ impl AssetCollector {
.map(|id| id.as_ident().map(|id| id.value.clone()))
.collect::<Option<Vec<String>>>()?
.join(".");
let path = format!("{}/{}", path, specific_table);
// For Resource assets, use ?table= query parameter syntax
// For Ducklake and DataTable, maintain /table syntax
let path = if *kind == AssetKind::Resource {
format!("{}?table={}", path, specific_table)
} else {
format!("{}/{}", path, specific_table)
};
return Some(ParseAssetsResult { kind: *kind, access_type, path, columns: None });
}
}
@@ -104,7 +111,14 @@ impl AssetCollector {
.map(|id| id.as_ident().map(|id| id.value.clone()))
.collect::<Option<Vec<String>>>()?
.join(".");
format!("{}/{}", path, specific_table)
// For Resource assets, use ?table= query parameter syntax
// For Ducklake and DataTable, maintain /table syntax
if *kind == AssetKind::Resource {
format!("{}?table={}", path, specific_table)
} else {
format!("{}/{}", path, specific_table)
}
} else {
path.clone()
};
@@ -845,7 +859,7 @@ mod tests {
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::Resource,
path: "u/user/pg_resource/table1".to_string(),
path: "u/user/pg_resource?table=table1".to_string(),
access_type: Some(R),
columns: None
},])
@@ -870,6 +884,75 @@ mod tests {
);
}
#[test]
fn test_sql_asset_parser_resource_vs_ducklake_syntax() {
// Test that Resource uses ?table= while Ducklake uses /table
let input_resource = r#"
ATTACH 'res://u/user/pg_resource' AS db (TYPE postgres);
SELECT * FROM db.users;
"#;
let s = parse_assets(input_resource).map(|s| s.assets);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::Resource,
path: "u/user/pg_resource?table=users".to_string(),
access_type: Some(R),
columns: None
},])
);
let input_ducklake = r#"
ATTACH 'ducklake://my_lake' AS dl;
SELECT * FROM dl.users;
"#;
let s = parse_assets(input_ducklake).map(|s| s.assets);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::Ducklake,
path: "my_lake/users".to_string(),
access_type: Some(R),
columns: None
},])
);
let input_datatable = r#"
ATTACH 'datatable://dt1' AS dt;
SELECT * FROM dt.users;
"#;
let s = parse_assets(input_datatable).map(|s| s.assets);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::DataTable,
path: "dt1/users".to_string(),
access_type: Some(R),
columns: None
},])
);
}
#[test]
fn test_sql_asset_parser_resource_with_long_path() {
// Test that Resource works with paths longer than 3 components
let input = r#"
ATTACH 'res://u/diego/a/b/c/my_postgres_resource' AS db (TYPE postgres);
USE db;
SELECT * FROM my_table;
"#;
let s = parse_assets(input).map(|s| s.assets);
assert_eq!(
s.map_err(|e| e.to_string()),
Ok(vec![ParseAssetsResult {
kind: AssetKind::Resource,
path: "u/diego/a/b/c/my_postgres_resource?table=my_table".to_string(),
access_type: Some(R),
columns: None
},])
);
}
#[test]
fn test_sql_asset_parser_table_with_schema() {
let input = r#"
@@ -114,13 +114,14 @@ fn merge_access_types(a: AssetUsageAccessType, b: AssetUsageAccessType) -> Asset
// But never used it. In that case we don't know which table is being used,
// but we still want to add the main datatable as an asset with unknown access type.
//
// This function takes care of the fact that assets can be suffixed (e.g. "main/users")
// This function takes care of the fact that assets can be suffixed (e.g. "main/users" or "u/user/resource?table=table1")
pub fn asset_was_used(assets: &Vec<ParseAssetsResult>, (kind, path): (AssetKind, &String)) -> bool {
assets.iter().any(|a| {
let a_path = a.path.as_str();
// Check for /table suffix (Ducklake, DataTable) or ?table= suffix (Resource)
let has_same_path_base = a_path
.strip_prefix(path)
.map(|p| p.starts_with('/'))
.map(|p| p.starts_with('/') || p.starts_with('?'))
.unwrap_or(false);
(has_same_path_base || a_path == path) && a.kind == kind
})
+7 -1
View File
@@ -184,7 +184,13 @@ async fn list_assets(
FROM asset
INNER JOIN asset_summary ON asset.path = asset_summary.path AND asset.kind = asset_summary.kind
LEFT JOIN resource ON asset.kind = 'resource'
AND array_to_string((string_to_array(asset.path, '/'))[1:3], '/') = resource.path
AND (
-- Extract base path before '?' for ?table= syntax
CASE
WHEN asset.path LIKE '%?%' THEN split_part(asset.path, '?', 1)
ELSE asset.path
END
) = resource.path
AND resource.workspace_id = $1
LEFT JOIN v2_job job ON asset.usage_kind = 'job'
AND asset.usage_path = job.id::text