@@ -107,7 +107,7 @@ pub enum DatabasesCommands {
107107 name_or_id : String ,
108108 } ,
109109
110- /// Load a parquet file into a managed database table
110+ /// Load a parquet file or a saved query result into a managed database table
111111 Load {
112112 /// SQL catalog alias of the target database (e.g. `--catalog airbnb`)
113113 #[ arg( long) ]
@@ -122,16 +122,21 @@ pub enum DatabasesCommands {
122122 table : String ,
123123
124124 /// Path to a local parquet file to upload and load
125- #[ arg( long, conflicts_with_all = [ "upload_id" , "url" ] ) ]
125+ #[ arg( long, conflicts_with_all = [ "upload_id" , "url" , "result" ] ) ]
126126 file : Option < String > ,
127127
128128 /// URL of a remote parquet file to download and load
129- #[ arg( long, conflicts_with_all = [ "file" , "upload_id" ] ) ]
129+ #[ arg( long, conflicts_with_all = [ "file" , "upload_id" , "result" ] ) ]
130130 url : Option < String > ,
131131
132132 /// Use a previously staged upload ID from `POST /v1/uploads` instead of uploading
133- #[ arg( long, conflicts_with_all = [ "file" , "url" ] ) ]
133+ #[ arg( long, conflicts_with_all = [ "file" , "url" , "result" ] ) ]
134134 upload_id : Option < String > ,
135+
136+ /// Load a saved query result by id (e.g. `--result rslt…`, from
137+ /// `hotdata results` or a query's `[result-id: …]` footer) instead of a file
138+ #[ arg( long, conflicts_with_all = [ "file" , "url" , "upload_id" ] ) ]
139+ result : Option < String > ,
135140 } ,
136141
137142 /// Manage tables inside a managed database
@@ -190,7 +195,7 @@ pub enum DatabaseTablesCommands {
190195 output : String ,
191196 } ,
192197
193- /// Load a parquet file into a table (creates or replaces the table)
198+ /// Load a parquet file or a saved query result into a table (creates or replaces the table)
194199 Load {
195200 /// Database id or name (defaults to current database)
196201 #[ arg( long) ]
@@ -204,16 +209,21 @@ pub enum DatabaseTablesCommands {
204209 schema : String ,
205210
206211 /// Path to a local parquet file to upload and load
207- #[ arg( long, conflicts_with_all = [ "upload_id" , "url" ] ) ]
212+ #[ arg( long, conflicts_with_all = [ "upload_id" , "url" , "result" ] ) ]
208213 file : Option < String > ,
209214
210215 /// URL of a remote parquet file to download and load
211- #[ arg( long, conflicts_with_all = [ "file" , "upload_id" ] ) ]
216+ #[ arg( long, conflicts_with_all = [ "file" , "upload_id" , "result" ] ) ]
212217 url : Option < String > ,
213218
214219 /// Use a previously staged upload ID from `POST /v1/uploads` instead of uploading
215- #[ arg( long, conflicts_with_all = [ "file" , "url" ] ) ]
220+ #[ arg( long, conflicts_with_all = [ "file" , "url" , "result" ] ) ]
216221 upload_id : Option < String > ,
222+
223+ /// Load a saved query result by id (e.g. `--result rslt…`, from
224+ /// `hotdata results` or a query's `[result-id: …]` footer) instead of a file
225+ #[ arg( long, conflicts_with_all = [ "file" , "url" , "upload_id" ] ) ]
226+ result : Option < String > ,
217227 } ,
218228
219229 /// Delete a table from a managed database
@@ -561,6 +571,13 @@ pub fn load_table_request(upload_id: &str) -> serde_json::Value {
561571 } )
562572}
563573
574+ pub fn load_table_request_from_result ( result_id : & str ) -> serde_json:: Value {
575+ serde_json:: json!( {
576+ "mode" : "replace" ,
577+ "result_id" : result_id,
578+ } )
579+ }
580+
564581/// Returns true when `path` looks like a parquet file by extension.
565582pub fn is_parquet_path ( path : & str ) -> bool {
566583 path. to_ascii_lowercase ( ) . ends_with ( ".parquet" )
@@ -1332,6 +1349,7 @@ pub fn tables_list(workspace_id: &str, database: Option<&str>, schema: Option<&s
13321349 }
13331350}
13341351
1352+ #[ allow( clippy:: too_many_arguments) ]
13351353pub fn tables_load (
13361354 workspace_id : & str ,
13371355 database : Option < & str > ,
@@ -1340,6 +1358,7 @@ pub fn tables_load(
13401358 file : Option < & str > ,
13411359 url : Option < & str > ,
13421360 upload_id : Option < & str > ,
1361+ result : Option < & str > ,
13431362) {
13441363 use crossterm:: style:: Stylize ;
13451364
@@ -1350,7 +1369,16 @@ pub fn tables_load(
13501369 && crate :: client:: credentials:: api_key_jwt_source ( & profile) . as_deref ( )
13511370 == Some ( "database_api_token" )
13521371 {
1353- tables_load_database_scoped ( workspace_id, database, table, schema, file, url, upload_id) ;
1372+ tables_load_database_scoped (
1373+ workspace_id,
1374+ database,
1375+ table,
1376+ schema,
1377+ file,
1378+ url,
1379+ upload_id,
1380+ result,
1381+ ) ;
13541382 return ;
13551383 }
13561384
@@ -1378,20 +1406,24 @@ pub fn tables_load(
13781406 let db = resolve_database ( & api, & lookup_key) ;
13791407 let schema = schema_name ( schema) ;
13801408
1381- // clap enforces mutual exclusion; only one of these is ever Some.
1382- let upload_id = match ( upload_id, file, url) {
1383- ( Some ( id) , None , None ) => id. to_string ( ) ,
1384- ( None , Some ( path) , None ) => upload_parquet_file ( & api, path) ,
1385- ( None , None , Some ( u) ) => upload_parquet_url ( & api, u) ,
1386- ( None , None , None ) => {
1387- eprintln ! ( "error: --file <path>, --url <url>, or --upload-id <id> is required" ) ;
1409+ // clap enforces mutual exclusion; only one source is ever Some. A file/URL
1410+ // is uploaded first and loaded by upload id; a result is loaded by reference
1411+ // with no upload step.
1412+ let body = match ( result, upload_id, file, url) {
1413+ ( Some ( result_id) , None , None , None ) => load_table_request_from_result ( result_id) ,
1414+ ( None , Some ( id) , None , None ) => load_table_request ( id) ,
1415+ ( None , None , Some ( path) , None ) => load_table_request ( & upload_parquet_file ( & api, path) ) ,
1416+ ( None , None , None , Some ( u) ) => load_table_request ( & upload_parquet_url ( & api, u) ) ,
1417+ ( None , None , None , None ) => {
1418+ eprintln ! (
1419+ "error: one of --file <path>, --url <url>, --upload-id <id>, or --result <result-id> is required"
1420+ ) ;
13881421 std:: process:: exit ( 1 ) ;
13891422 }
13901423 _ => unreachable ! ( ) ,
13911424 } ;
13921425
13931426 let path = managed_table_load_path ( & db. default_connection_id , schema, table) ;
1394- let body = load_table_request ( & upload_id) ;
13951427
13961428 let spinner = crate :: util:: spinner ( "Loading table..." ) ;
13971429 let ( status, resp_body) = api. post_raw ( & path, & body) . unwrap_or_else ( |e| {
@@ -1565,6 +1597,7 @@ pub fn tables_load(
15651597/// Load path for a database API token: addresses the database by id and uses
15661598/// the database-scoped endpoints (the connection-scoped managed paths and the
15671599/// name/catalog resolve used by the standard flow are denied for this token).
1600+ #[ allow( clippy:: too_many_arguments) ]
15681601fn tables_load_database_scoped (
15691602 workspace_id : & str ,
15701603 database : Option < & str > ,
@@ -1573,6 +1606,7 @@ fn tables_load_database_scoped(
15731606 file : Option < & str > ,
15741607 url : Option < & str > ,
15751608 upload_id : Option < & str > ,
1609+ result : Option < & str > ,
15761610) {
15771611 use crossterm:: style:: Stylize ;
15781612
@@ -1596,20 +1630,24 @@ fn tables_load_database_scoped(
15961630 std:: process:: exit ( 1 ) ;
15971631 } ) ;
15981632
1599- // clap enforces mutual exclusion; only one of these is ever Some.
1600- let upload_id = match ( upload_id, file, url) {
1601- ( Some ( id) , None , None ) => id. to_string ( ) ,
1602- ( None , Some ( path) , None ) => upload_parquet_file ( & api, path) ,
1603- ( None , None , Some ( u) ) => upload_parquet_url ( & api, u) ,
1604- ( None , None , None ) => {
1605- eprintln ! ( "error: --file <path>, --url <url>, or --upload-id <id> is required" ) ;
1633+ // clap enforces mutual exclusion; only one source is ever Some. A file/URL
1634+ // is uploaded first and loaded by upload id; a result is loaded by reference
1635+ // with no upload step.
1636+ let body = match ( result, upload_id, file, url) {
1637+ ( Some ( result_id) , None , None , None ) => load_table_request_from_result ( result_id) ,
1638+ ( None , Some ( id) , None , None ) => load_table_request ( id) ,
1639+ ( None , None , Some ( path) , None ) => load_table_request ( & upload_parquet_file ( & api, path) ) ,
1640+ ( None , None , None , Some ( u) ) => load_table_request ( & upload_parquet_url ( & api, u) ) ,
1641+ ( None , None , None , None ) => {
1642+ eprintln ! (
1643+ "error: one of --file <path>, --url <url>, --upload-id <id>, or --result <result-id> is required"
1644+ ) ;
16061645 std:: process:: exit ( 1 ) ;
16071646 }
16081647 _ => unreachable ! ( ) ,
16091648 } ;
16101649
16111650 let load_path = database_table_load_path ( & db_id, schema, table) ;
1612- let body = load_table_request ( & upload_id) ;
16131651
16141652 let load = || {
16151653 let spinner = crate :: util:: spinner ( "Loading table..." ) ;
@@ -1960,6 +1998,15 @@ mod tests {
19601998 assert_eq ! ( body[ "upload_id" ] , "upl_abc" ) ;
19611999 }
19622000
2001+ #[ test]
2002+ fn load_table_request_from_result_is_replace_mode ( ) {
2003+ let body = load_table_request_from_result ( "rslt_abc" ) ;
2004+ assert_eq ! ( body[ "mode" ] , "replace" ) ;
2005+ assert_eq ! ( body[ "result_id" ] , "rslt_abc" ) ;
2006+ // A result load must not send an upload_id (the server rejects both).
2007+ assert ! ( body. get( "upload_id" ) . is_none( ) ) ;
2008+ }
2009+
19632010 #[ test]
19642011 fn is_parquet_path_by_extension ( ) {
19652012 assert ! ( is_parquet_path( "/data/orders.parquet" ) ) ;
@@ -2097,6 +2144,50 @@ mod tests {
20972144 load. assert ( ) ;
20982145 }
20992146
2147+ #[ test]
2148+ fn tables_load_from_result_posts_result_id ( ) {
2149+ let mut server = mockito:: Server :: new ( ) ;
2150+ let resolve = server
2151+ . mock ( "GET" , "/v1/databases/db_1" )
2152+ . with_status ( 200 )
2153+ . with_header ( "content-type" , "application/json" )
2154+ . with_body ( full_detail ( "db_1" , "sales" , "conn_default" ) )
2155+ . create ( ) ;
2156+ // A result load hits the same loads endpoint but with a result_id body
2157+ // and no upload step (no POST /v1/uploads mock is registered).
2158+ let load = server
2159+ . mock (
2160+ "POST" ,
2161+ "/v1/connections/conn_default/schemas/public/tables/orders/loads" ,
2162+ )
2163+ . match_body ( mockito:: Matcher :: JsonString (
2164+ serde_json:: to_string ( & load_table_request_from_result ( "rslt_123" ) ) . unwrap ( ) ,
2165+ ) )
2166+ . with_status ( 200 )
2167+ . with_body (
2168+ r#"{
2169+ "connection_id":"conn_default",
2170+ "schema_name":"public",
2171+ "table_name":"orders",
2172+ "row_count":42,
2173+ "arrow_schema_json":"{}"
2174+ }"# ,
2175+ )
2176+ . create ( ) ;
2177+
2178+ let api = Api :: test_new ( & server. url ( ) , "k" , Some ( "ws1" ) ) ;
2179+ let db = resolve_database ( & api, "db_1" ) ;
2180+ let path = managed_table_load_path ( & db. default_connection_id , "public" , "orders" ) ;
2181+ let body = load_table_request_from_result ( "rslt_123" ) ;
2182+ let ( status, resp_body) = api. post_raw ( & path, & body) . unwrap ( ) ;
2183+ assert ! ( status. is_success( ) ) ;
2184+ let parsed: LoadManagedTableResponse = serde_json:: from_str ( & resp_body) . unwrap ( ) ;
2185+ assert_eq ! ( parsed. row_count, 42 ) ;
2186+ assert_eq ! ( parsed. table_name, "orders" ) ;
2187+ resolve. assert ( ) ;
2188+ load. assert ( ) ;
2189+ }
2190+
21002191 #[ test]
21012192 fn tables_delete_uses_default_connection_id ( ) {
21022193 let mut server = mockito:: Server :: new ( ) ;
0 commit comments