diff --git a/os-apps/paw-agent/wasm/monty_repl/src/entity_ops.rs b/os-apps/paw-agent/wasm/monty_repl/src/entity_ops.rs index bd35ebefa..cc0fea13d 100644 --- a/os-apps/paw-agent/wasm/monty_repl/src/entity_ops.rs +++ b/os-apps/paw-agent/wasm/monty_repl/src/entity_ops.rs @@ -2573,8 +2573,11 @@ fn pawfs_filter_parent_and_workspace(parent_id: &str, ws_id: &str) -> String { } fn pawfs_filter_root_directory(ws_id: &str) -> String { + // `ParentId eq null` is non-lossless -> disables index pushdown -> full Directories + // scan -> 413 QueryTooLarge at workspace scale (ARN-68). Name '/' + WorkspaceId already + // uniquely identify the root, so omit the null predicate to keep the read pushed-down. format!( - "Name eq '/' and WorkspaceId eq '{}' and ParentId eq null", + "Name eq '/' and WorkspaceId eq '{}'", escape_odata_string(ws_id) ) } @@ -3443,10 +3446,14 @@ mod tests { #[test] fn pawfs_walk_filters_use_parent_and_directory_pushdown() { + // ARN-68: the root filter must NOT use the non-lossless `ParentId eq null` + // (it disables index pushdown -> full Directories scan -> 413). Name '/' + + // WorkspaceId uniquely identify the root. assert_eq!( pawfs_filter_root_directory("os-app-docs"), - "Name eq '/' and WorkspaceId eq 'os-app-docs' and ParentId eq null" + "Name eq '/' and WorkspaceId eq 'os-app-docs'" ); + assert!(!pawfs_filter_root_directory("os-app-docs").contains("eq null")); assert_eq!( pawfs_filter_name_parent_and_workspace("skills", "dir-parent", "os-app-docs"), "Name eq 'skills' and ParentId eq 'dir-parent' and WorkspaceId eq 'os-app-docs'" diff --git a/os-apps/paw-fs/wasm/workspace_fs/src/ops.rs b/os-apps/paw-fs/wasm/workspace_fs/src/ops.rs index 6f608fae3..3d95a1a6c 100644 --- a/os-apps/paw-fs/wasm/workspace_fs/src/ops.rs +++ b/os-apps/paw-fs/wasm/workspace_fs/src/ops.rs @@ -67,18 +67,31 @@ fn find_directory( parent_id: Option<&str>, ) -> Result, String> { let name_enc = odata_encode(name); + // `ParentId eq ''` is a lossless string-equality that pushes down. But the root's + // old `ParentId eq null` is NON-LOSSLESS -> disables index pushdown -> full Directories + // scan -> 413 QueryTooLarge at workspace scale (ARN-68). Name + WorkspaceId already + // uniquely identify the root, so omit the null predicate (lossless pushdown) and confirm + // ParentId is unset in-memory. let parent_clause = match parent_id { Some(pid) => format!("%20and%20ParentId%20eq%20'{}'", odata_encode(pid)), - None => "%20and%20ParentId%20eq%20null".to_string(), + None => String::new(), }; let url = format!( "{api_url}/tdata/Directories?$filter=Name%20eq%20'{name_enc}'%20and%20WorkspaceId%20eq%20'{ws_id}'{parent_clause}" ); let resp = http_get(ctx, &url, tenant)?; let items = resp.get("value").and_then(|v| v.as_array()); - Ok(items - .and_then(|arr| arr.first()) - .and_then(|v| extract_id(v))) + let chosen = items.and_then(|arr| { + arr.iter().find(|v| match parent_id { + // root: accept only an entry whose ParentId is null / absent / empty + None => v + .get("ParentId") + .map_or(true, |p| p.is_null() || p.as_str().map_or(true, str::is_empty)), + // a specific parent is already constrained by the query + Some(_) => true, + }) + }); + Ok(chosen.and_then(extract_id)) } /// Find a file by Path + WorkspaceId.