From 29514d6b30f4687bb5f3006d867bc9a903e91d7b Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Thu, 19 Mar 2026 13:13:46 -0400 Subject: [PATCH 01/76] chore(rust): add adbc-snowflake placeholder crate --- rust/adbc-snowflake/Cargo.toml | 28 ++++++++++++++++++++++++++ rust/adbc-snowflake/src/lib.rs | 36 ++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 rust/adbc-snowflake/Cargo.toml create mode 100644 rust/adbc-snowflake/src/lib.rs diff --git a/rust/adbc-snowflake/Cargo.toml b/rust/adbc-snowflake/Cargo.toml new file mode 100644 index 0000000..9e92d36 --- /dev/null +++ b/rust/adbc-snowflake/Cargo.toml @@ -0,0 +1,28 @@ +# Copyright (c) 2026 ADBC Drivers Contributors +# +# This file has been modified from its original version, which is +# under the Apache License: +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +[package] +name = "adbc-snowflake" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/rust/adbc-snowflake/src/lib.rs b/rust/adbc-snowflake/src/lib.rs new file mode 100644 index 0000000..90e49e7 --- /dev/null +++ b/rust/adbc-snowflake/src/lib.rs @@ -0,0 +1,36 @@ +// Copyright (c) 2026 ADBC Drivers Contributors +// +// This file has been modified from its original version, which is +// under the Apache License: +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} From a7c046ab48bd361a80cc9b512abd96c2181a0698 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Thu, 19 Mar 2026 13:16:53 -0400 Subject: [PATCH 02/76] chore(rust): update Cargo.toml for adbc-snowflake driver --- rust/adbc-snowflake/Cargo.toml | 31 ++++++++------------------- rust/adbc-snowflake/src/lib.rs | 38 ++-------------------------------- 2 files changed, 11 insertions(+), 58 deletions(-) diff --git a/rust/adbc-snowflake/Cargo.toml b/rust/adbc-snowflake/Cargo.toml index 9e92d36..91cbbd3 100644 --- a/rust/adbc-snowflake/Cargo.toml +++ b/rust/adbc-snowflake/Cargo.toml @@ -1,28 +1,15 @@ -# Copyright (c) 2026 ADBC Drivers Contributors -# -# This file has been modified from its original version, which is -# under the Apache License: -# -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. - [package] name = "adbc-snowflake" version = "0.1.0" edition = "2024" +[lib] +crate-type = ["cdylib", "rlib"] + [dependencies] +adbc_core = "0.22.0" +adbc_ffi = "0.22.0" +sf_core = { git = "https://github.com/snowflakedb/universal-driver", subdirectory = "sf_core" } +arrow-array = { version = ">=53.1.0, <59", default-features = false, features = ["ffi"] } +arrow-schema = { version = ">=53.1.0, <59", default-features = false } +tokio = { version = "1", features = ["rt-multi-thread"] } diff --git a/rust/adbc-snowflake/src/lib.rs b/rust/adbc-snowflake/src/lib.rs index 90e49e7..889ab6f 100644 --- a/rust/adbc-snowflake/src/lib.rs +++ b/rust/adbc-snowflake/src/lib.rs @@ -1,36 +1,2 @@ -// Copyright (c) 2026 ADBC Drivers Contributors -// -// This file has been modified from its original version, which is -// under the Apache License: -// -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -pub fn add(left: u64, right: u64) -> u64 { - left + right -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} +// src/lib.rs +pub fn placeholder() {} From 946cb6d76425b0e6d0785339a5d8d047f4d56786 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Thu, 19 Mar 2026 13:34:05 -0400 Subject: [PATCH 03/76] feat(rust): implement error conversion for adbc-snowflake Implement From trait to convert sf_core errors to adbc_core errors, with exhaustive mapping of all 16 ApiError variants to appropriate Status codes. Also add not_implemented() helper for NotImplemented errors. --- rust/adbc-snowflake/src/error.rs | 54 ++++++++++++++++++++++++++++++++ rust/adbc-snowflake/src/lib.rs | 2 ++ 2 files changed, 56 insertions(+) create mode 100644 rust/adbc-snowflake/src/error.rs diff --git a/rust/adbc-snowflake/src/error.rs b/rust/adbc-snowflake/src/error.rs new file mode 100644 index 0000000..3c7dfe3 --- /dev/null +++ b/rust/adbc-snowflake/src/error.rs @@ -0,0 +1,54 @@ +// src/error.rs +use adbc_core::error::{Error, Status}; +use sf_core::apis::database_driver_v1::ApiError; + +pub(crate) fn api_error_to_adbc_error(err: ApiError) -> Error { + let status = match &err { + ApiError::InvalidArgument { .. } => Status::InvalidArguments, + ApiError::Configuration { .. } => Status::InvalidArguments, + ApiError::ConnectionNotInitialized { .. } => Status::InvalidState, + ApiError::ConnectionLocking { .. } => Status::InvalidState, + ApiError::StatementLocking { .. } => Status::InvalidState, + ApiError::DatabaseLocking { .. } => Status::InvalidState, + ApiError::InvalidRefreshState { .. } => Status::InvalidState, + ApiError::Login { .. } => Status::Unauthenticated, + ApiError::SessionRefresh { .. } => Status::Unauthenticated, + ApiError::MasterTokenExpired { .. } => Status::Unauthenticated, + ApiError::TlsClientCreation { .. } => Status::IO, + ApiError::Query { .. } => Status::IO, + ApiError::QueryResponseProcessing { .. } => Status::IO, + ApiError::Statement { .. } => Status::IO, + ApiError::RuntimeCreation { .. } => Status::IO, + ApiError::GenericError { .. } => Status::IO, + }; + Error::with_message_and_status(err.to_string(), status) +} + +pub(crate) fn not_implemented(msg: &str) -> Error { + Error::with_message_and_status(msg, Status::NotImplemented) +} + +#[cfg(test)] +mod tests { + use super::*; + use adbc_core::error::Status; + + #[test] + fn invalid_argument_maps_to_invalid_arguments() { + use sf_core::apis::database_driver_v1::ApiError; + // Build an InvalidArgument error via the snafu builder pattern + let err: ApiError = sf_core::apis::database_driver_v1::error::InvalidArgumentSnafu { + argument: "test".to_string(), + } + .build(); + let adbc_err = api_error_to_adbc_error(err); + assert_eq!(adbc_err.status, Status::InvalidArguments); + } + + #[test] + fn not_implemented_returns_correct_status() { + let err = not_implemented("foo"); + assert_eq!(err.status, adbc_core::error::Status::NotImplemented); + assert!(err.message.contains("foo")); + } +} diff --git a/rust/adbc-snowflake/src/lib.rs b/rust/adbc-snowflake/src/lib.rs index 889ab6f..001cb16 100644 --- a/rust/adbc-snowflake/src/lib.rs +++ b/rust/adbc-snowflake/src/lib.rs @@ -1,2 +1,4 @@ // src/lib.rs +mod error; + pub fn placeholder() {} From 4b41b0dedb263e20b64db280b72a29a2bc664bdc Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Thu, 19 Mar 2026 14:04:08 -0400 Subject: [PATCH 04/76] feat(rust): implement Driver with Inner (runtime + sf_core) - Add src/driver.rs: Inner struct (tokio Runtime + DatabaseDriverV1) and Driver wrapping Arc, implementing adbc_core::Driver trait - Add stub src/database.rs, src/connection.rs, src/statement.rs that compile with todo!() bodies for later tasks - Update src/lib.rs to expose Driver, Database, Connection, Statement - Fix arrow-array/schema version constraints to match adbc_core (57.x) to avoid duplicate crate instance trait bound failures - Fix error.rs test to use public sf_core APIs instead of pub(crate) snafu builder internals (InvalidArgumentSnafu) - Add .cargo/config.toml with prefer-dynamic=no to work around sf_core dylib linker issues when building test binaries 2 driver tests pass: driver_default_creates_successfully, new_database_succeeds_with_no_options --- rust/adbc-snowflake/.cargo/config.toml | 4 + rust/adbc-snowflake/Cargo.lock | 5197 ++++++++++++++++++++++++ rust/adbc-snowflake/Cargo.toml | 4 +- rust/adbc-snowflake/src/connection.rs | 40 + rust/adbc-snowflake/src/database.rs | 29 + rust/adbc-snowflake/src/driver.rs | 86 + rust/adbc-snowflake/src/error.rs | 11 +- rust/adbc-snowflake/src/lib.rs | 12 +- rust/adbc-snowflake/src/statement.rs | 47 + 9 files changed, 5421 insertions(+), 9 deletions(-) create mode 100644 rust/adbc-snowflake/.cargo/config.toml create mode 100644 rust/adbc-snowflake/Cargo.lock create mode 100644 rust/adbc-snowflake/src/connection.rs create mode 100644 rust/adbc-snowflake/src/database.rs create mode 100644 rust/adbc-snowflake/src/driver.rs create mode 100644 rust/adbc-snowflake/src/statement.rs diff --git a/rust/adbc-snowflake/.cargo/config.toml b/rust/adbc-snowflake/.cargo/config.toml new file mode 100644 index 0000000..5e8fa28 --- /dev/null +++ b/rust/adbc-snowflake/.cargo/config.toml @@ -0,0 +1,4 @@ +# Disable dynamic linking to avoid linker errors from sf_core's dylib crate-type +# when building test binaries on Linux. +[build] +rustflags = ["-C", "prefer-dynamic=no"] diff --git a/rust/adbc-snowflake/Cargo.lock b/rust/adbc-snowflake/Cargo.lock new file mode 100644 index 0000000..e1fe3cb --- /dev/null +++ b/rust/adbc-snowflake/Cargo.lock @@ -0,0 +1,5197 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adbc-snowflake" +version = "0.1.0" +dependencies = [ + "adbc_core", + "adbc_ffi", + "arrow-array 57.3.0", + "arrow-schema 57.3.0", + "sf_core", + "tokio", +] + +[[package]] +name = "adbc_core" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8dbe031527c9856a1e2df5e82aa8e568ffaab3be897f70d874477fb42a783bb" +dependencies = [ + "arrow-array 57.3.0", + "arrow-schema 57.3.0", +] + +[[package]] +name = "adbc_ffi" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3600ae9aec2907516d088189e3b863029280f1953dd0eab903c7f4c862a0ce81" +dependencies = [ + "adbc_core", + "arrow-array 57.3.0", + "arrow-schema 57.3.0", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "const-random", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse 0.2.7", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse 1.0.0", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arrow" +version = "56.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e833808ff2d94ed40d9379848a950d995043c7fb3e81a30b383f4c6033821cc" +dependencies = [ + "arrow-arith", + "arrow-array 56.2.0", + "arrow-buffer 56.2.0", + "arrow-cast", + "arrow-csv", + "arrow-data 56.2.0", + "arrow-ipc", + "arrow-json", + "arrow-ord", + "arrow-row", + "arrow-schema 56.2.0", + "arrow-select", + "arrow-string", +] + +[[package]] +name = "arrow-arith" +version = "56.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad08897b81588f60ba983e3ca39bda2b179bdd84dced378e7df81a5313802ef8" +dependencies = [ + "arrow-array 56.2.0", + "arrow-buffer 56.2.0", + "arrow-data 56.2.0", + "arrow-schema 56.2.0", + "chrono", + "num", +] + +[[package]] +name = "arrow-array" +version = "56.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8548ca7c070d8db9ce7aa43f37393e4bfcf3f2d3681df278490772fd1673d08d" +dependencies = [ + "ahash", + "arrow-buffer 56.2.0", + "arrow-data 56.2.0", + "arrow-schema 56.2.0", + "chrono", + "half", + "hashbrown 0.16.1", + "num", +] + +[[package]] +name = "arrow-array" +version = "57.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8955af33b25f3b175ee10af580577280b4bd01f7e823d94c7cdef7cf8c9aef" +dependencies = [ + "ahash", + "arrow-buffer 57.3.0", + "arrow-data 57.3.0", + "arrow-schema 57.3.0", + "chrono", + "half", + "hashbrown 0.16.1", + "num-complex", + "num-integer", + "num-traits", +] + +[[package]] +name = "arrow-buffer" +version = "56.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e003216336f70446457e280807a73899dd822feaf02087d31febca1363e2fccc" +dependencies = [ + "bytes", + "half", + "num", +] + +[[package]] +name = "arrow-buffer" +version = "57.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c697ddca96183182f35b3a18e50b9110b11e916d7b7799cbfd4d34662f2c56c2" +dependencies = [ + "bytes", + "half", + "num-bigint", + "num-traits", +] + +[[package]] +name = "arrow-cast" +version = "56.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "919418a0681298d3a77d1a315f625916cb5678ad0d74b9c60108eb15fd083023" +dependencies = [ + "arrow-array 56.2.0", + "arrow-buffer 56.2.0", + "arrow-data 56.2.0", + "arrow-schema 56.2.0", + "arrow-select", + "atoi", + "base64 0.22.1", + "chrono", + "half", + "lexical-core", + "num", + "ryu", +] + +[[package]] +name = "arrow-csv" +version = "56.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa9bf02705b5cf762b6f764c65f04ae9082c7cfc4e96e0c33548ee3f67012eb" +dependencies = [ + "arrow-array 56.2.0", + "arrow-cast", + "arrow-schema 56.2.0", + "chrono", + "csv", + "csv-core", + "regex", +] + +[[package]] +name = "arrow-data" +version = "56.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5c64fff1d142f833d78897a772f2e5b55b36cb3e6320376f0961ab0db7bd6d0" +dependencies = [ + "arrow-buffer 56.2.0", + "arrow-schema 56.2.0", + "half", + "num", +] + +[[package]] +name = "arrow-data" +version = "57.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fdd994a9d28e6365aa78e15da3f3950c0fdcea6b963a12fa1c391afb637b304" +dependencies = [ + "arrow-buffer 57.3.0", + "arrow-schema 57.3.0", + "half", + "num-integer", + "num-traits", +] + +[[package]] +name = "arrow-ipc" +version = "56.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d3594dcddccc7f20fd069bc8e9828ce37220372680ff638c5e00dea427d88f5" +dependencies = [ + "arrow-array 56.2.0", + "arrow-buffer 56.2.0", + "arrow-data 56.2.0", + "arrow-schema 56.2.0", + "arrow-select", + "flatbuffers", +] + +[[package]] +name = "arrow-json" +version = "56.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88cf36502b64a127dc659e3b305f1d993a544eab0d48cce704424e62074dc04b" +dependencies = [ + "arrow-array 56.2.0", + "arrow-buffer 56.2.0", + "arrow-cast", + "arrow-data 56.2.0", + "arrow-schema 56.2.0", + "chrono", + "half", + "indexmap", + "lexical-core", + "memchr", + "num", + "serde", + "serde_json", + "simdutf8", +] + +[[package]] +name = "arrow-ord" +version = "56.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8f82583eb4f8d84d4ee55fd1cb306720cddead7596edce95b50ee418edf66f" +dependencies = [ + "arrow-array 56.2.0", + "arrow-buffer 56.2.0", + "arrow-data 56.2.0", + "arrow-schema 56.2.0", + "arrow-select", +] + +[[package]] +name = "arrow-row" +version = "56.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d07ba24522229d9085031df6b94605e0f4b26e099fb7cdeec37abd941a73753" +dependencies = [ + "arrow-array 56.2.0", + "arrow-buffer 56.2.0", + "arrow-data 56.2.0", + "arrow-schema 56.2.0", + "half", +] + +[[package]] +name = "arrow-schema" +version = "56.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3aa9e59c611ebc291c28582077ef25c97f1975383f1479b12f3b9ffee2ffabe" +dependencies = [ + "bitflags", +] + +[[package]] +name = "arrow-schema" +version = "57.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c872d36b7bf2a6a6a2b40de9156265f0242910791db366a2c17476ba8330d68" +dependencies = [ + "bitflags", +] + +[[package]] +name = "arrow-select" +version = "56.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c41dbbd1e97bfcaee4fcb30e29105fb2c75e4d82ae4de70b792a5d3f66b2e7a" +dependencies = [ + "ahash", + "arrow-array 56.2.0", + "arrow-buffer 56.2.0", + "arrow-data 56.2.0", + "arrow-schema 56.2.0", + "num", +] + +[[package]] +name = "arrow-string" +version = "56.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53f5183c150fbc619eede22b861ea7c0eebed8eaac0333eaa7f6da5205fd504d" +dependencies = [ + "arrow-array 56.2.0", + "arrow-buffer 56.2.0", + "arrow-data 56.2.0", + "arrow-schema 56.2.0", + "arrow-select", + "memchr", + "num", + "regex", + "regex-syntax", +] + +[[package]] +name = "asn1-rs" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "aws-config" +version = "1.8.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c456581cb3c77fafcc8c67204a70680d40b61112d6da78c77bd31d945b65f1b5" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sdk-sso", + "aws-sdk-ssooidc", + "aws-sdk-sts", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "hex", + "http 1.4.0", + "ring", + "time", + "tokio", + "tracing", + "url", + "zeroize", +] + +[[package]] +name = "aws-credential-types" +version = "1.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cd362783681b15d136480ad555a099e82ecd8e2d10a841e14dfd0078d67fee3" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "zeroize", +] + +[[package]] +name = "aws-lc-rs" +version = "1.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf" +dependencies = [ + "aws-lc-sys", + "untrusted 0.7.1", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "aws-runtime" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c635c2dc792cb4a11ce1a4f392a925340d1bdf499289b5ec1ec6810954eb43f5" +dependencies = [ + "aws-credential-types", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "http-body 1.0.1", + "percent-encoding", + "pin-project-lite", + "tracing", + "uuid", +] + +[[package]] +name = "aws-sdk-s3" +version = "1.122.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94c2ca0cba97e8e279eb6c0b2d0aa10db5959000e602ab2b7c02de6b85d4c19b" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-checksums", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "bytes", + "fastrand", + "hex", + "hmac", + "http 0.2.12", + "http 1.4.0", + "http-body 1.0.1", + "lru 0.16.3", + "percent-encoding", + "regex-lite", + "sha2", + "tracing", + "url", +] + +[[package]] +name = "aws-sdk-sso" +version = "1.93.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dcb38bb33fc0a11f1ffc3e3e85669e0a11a37690b86f77e75306d8f369146a0" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-ssooidc" +version = "1.95.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ada8ffbea7bd1be1f53df1dadb0f8fdb04badb13185b3321b929d1ee3caad09" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sts" +version = "1.97.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6443ccadc777095d5ed13e21f5c364878c9f5bad4e35187a6cdbd863b0afcad" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-query", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sigv4" +version = "1.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efa49f3c607b92daae0c078d48a4571f599f966dce3caee5f1ea55c4d9073f99" +dependencies = [ + "aws-credential-types", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "crypto-bigint 0.5.5", + "form_urlencoded", + "hex", + "hmac", + "http 0.2.12", + "http 1.4.0", + "p256", + "percent-encoding", + "ring", + "sha2", + "subtle", + "time", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-async" +version = "1.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52eec3db979d18cb807fc1070961cc51d87d069abe9ab57917769687368a8c6c" +dependencies = [ + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "aws-smithy-checksums" +version = "0.64.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddcf418858f9f3edd228acb8759d77394fed7531cce78d02bdda499025368439" +dependencies = [ + "aws-smithy-http", + "aws-smithy-types", + "bytes", + "crc-fast", + "hex", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "md-5", + "pin-project-lite", + "sha1", + "sha2", + "tracing", +] + +[[package]] +name = "aws-smithy-eventstream" +version = "0.60.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35b9c7354a3b13c66f60fe4616d6d1969c9fd36b1b5333a5dfb3ee716b33c588" +dependencies = [ + "aws-smithy-types", + "bytes", + "crc32fast", +] + +[[package]] +name = "aws-smithy-http" +version = "0.63.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630e67f2a31094ffa51b210ae030855cb8f3b7ee1329bdd8d085aaf61e8b97fc" +dependencies = [ + "aws-smithy-eventstream", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tracing", +] + +[[package]] +name = "aws-smithy-http-client" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12fb0abf49ff0cab20fd31ac1215ed7ce0ea92286ba09e2854b42ba5cabe7525" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "h2 0.3.27", + "h2 0.4.13", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper 1.8.1", + "hyper-rustls 0.24.2", + "hyper-rustls 0.27.7", + "hyper-util", + "pin-project-lite", + "rustls 0.21.12", + "rustls 0.23.37", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.4", + "tower", + "tracing", +] + +[[package]] +name = "aws-smithy-json" +version = "0.62.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cb96aa208d62ee94104645f7b2ecaf77bf27edf161590b6224bfbac2832f979" +dependencies = [ + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-observability" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0a46543fbc94621080b3cf553eb4cbbdc41dd9780a30c4756400f0139440a1d" +dependencies = [ + "aws-smithy-runtime-api", +] + +[[package]] +name = "aws-smithy-query" +version = "0.60.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cebbddb6f3a5bd81553643e9c7daf3cc3dc5b0b5f398ac668630e8a84e6fff0" +dependencies = [ + "aws-smithy-types", + "urlencoding", +] + +[[package]] +name = "aws-smithy-runtime" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3df87c14f0127a0d77eb261c3bc45d5b4833e2a1f63583ebfb728e4852134ee" +dependencies = [ + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-http-client", + "aws-smithy-observability", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", + "pin-project-lite", + "pin-utils", + "tokio", + "tracing", +] + +[[package]] +name = "aws-smithy-runtime-api" +version = "1.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49952c52f7eebb72ce2a754d3866cc0f87b97d2a46146b79f80f3a93fb2b3716" +dependencies = [ + "aws-smithy-async", + "aws-smithy-types", + "bytes", + "http 0.2.12", + "http 1.4.0", + "pin-project-lite", + "tokio", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-types" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3a26048eeab0ddeba4b4f9d51654c79af8c3b32357dc5f336cee85ab331c33" +dependencies = [ + "base64-simd", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", + "itoa", + "num-integer", + "pin-project-lite", + "pin-utils", + "ryu", + "serde", + "time", + "tokio", + "tokio-util", +] + +[[package]] +name = "aws-smithy-xml" +version = "0.60.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11b2f670422ff42bf7065031e72b45bc52a3508bd089f743ea90731ca2b6ea57" +dependencies = [ + "xmlparser", +] + +[[package]] +name = "aws-types" +version = "1.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d980627d2dd7bfc32a3c025685a033eeab8d365cc840c631ef59d1b8f428164" +dependencies = [ + "aws-credential-types", + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "rustc_version", + "tracing", +] + +[[package]] +name = "base16ct" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" +dependencies = [ + "outref", + "vsimd", +] + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "bytes-utils" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" +dependencies = [ + "bytes", + "either", +] + +[[package]] +name = "cc" +version = "1.2.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream 1.0.0", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "cmake" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +dependencies = [ + "cc", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.17", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc-fast" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd92aca2c6001b1bf5ba0ff84ee74ec8501b52bbef0cac80bf25a6c1d87a83d" +dependencies = [ + "crc", + "digest", + "rustversion", + "spin", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "csv" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde_core", +] + +[[package]] +name = "csv-core" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" +dependencies = [ + "memchr", +] + +[[package]] +name = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + +[[package]] +name = "der" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "der_derive", + "flagset", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "der-parser" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "der_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034092389675178f570469e6c3b0465d3d30b4505c294a6550db47f3c17ad18" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "doc-comment" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "780955b8b195a21ab8e4ac6b60dd1dbdcec1dc6c51c0617964b08c81785e12c9" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "ecdsa" +version = "0.14.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" +dependencies = [ + "der 0.6.1", + "elliptic-curve", + "rfc6979", + "signature 1.6.4", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "elliptic-curve" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" +dependencies = [ + "base16ct", + "crypto-bigint 0.4.9", + "der 0.6.1", + "digest", + "ff", + "generic-array", + "group", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_filter" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" +dependencies = [ + "anstream 0.6.21", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "error_trace" +version = "0.1.0" +source = "git+https://github.com/snowflakedb/universal-driver#080422e05fbd727f68d7c494e564ec625e1375d6" +dependencies = [ + "error_trace_derive", + "snafu 0.8.9", +] + +[[package]] +name = "error_trace_derive" +version = "0.1.0" +source = "git+https://github.com/snowflakedb/universal-driver#080422e05fbd727f68d7c494e564ec625e1375d6" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "ff" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flagset" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ac824320a75a52197e8f2d787f6a38b6718bb6897a35142d749af3c0e8f4fe" + +[[package]] +name = "flatbuffers" +version = "25.12.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35f6839d7b3b98adde531effaf34f0c2badc6f4735d26fe74709d8e513a96ef3" +dependencies = [ + "bitflags", + "rustc_version", +] + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "group" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.4.0", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "num-traits", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "html-escape" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476" +dependencies = [ + "utf8-width", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.4.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http 1.4.0", + "http-body 1.0.1", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.27", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2 0.4.13", + "http 1.4.0", + "http-body 1.0.1", + "httparse", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.32", + "log", + "rustls 0.21.12", + "tokio", + "tokio-rustls 0.24.1", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http 1.4.0", + "hyper 1.8.1", + "hyper-util", + "rustls 0.23.37", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.4", + "tower-service", + "webpki-roots", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.8.1", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "hyper 1.8.1", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.3", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "jiff" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "jwt" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6204285f77fe7d9784db3fdc449ecce1a0114927a51d5a41c4c7a292011c015f" +dependencies = [ + "base64 0.13.1", + "crypto-common", + "digest", + "hmac", + "openssl", + "serde", + "serde_json", + "sha2", +] + +[[package]] +name = "keyring" +version = "3.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" +dependencies = [ + "byteorder", + "linux-keyutils", + "log", + "security-framework 2.11.1", + "security-framework 3.7.0", + "windows-sys 0.60.2", + "zeroize", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "lexical-core" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8d125a277f807e55a77304455eb7b1cb52f2b18c143b60e766c120bd64a594" +dependencies = [ + "lexical-parse-float", + "lexical-parse-integer", + "lexical-util", + "lexical-write-float", + "lexical-write-integer", +] + +[[package]] +name = "lexical-parse-float" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52a9f232fbd6f550bc0137dcb5f99ab674071ac2d690ac69704593cb4abbea56" +dependencies = [ + "lexical-parse-integer", + "lexical-util", +] + +[[package]] +name = "lexical-parse-integer" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a7a039f8fb9c19c996cd7b2fcce303c1b2874fe1aca544edc85c4a5f8489b34" +dependencies = [ + "lexical-util", +] + +[[package]] +name = "lexical-util" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2604dd126bb14f13fb5d1bd6a66155079cb9fa655b37f875b3a742c705dbed17" + +[[package]] +name = "lexical-write-float" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c438c87c013188d415fbabbb1dceb44249ab81664efbd31b14ae55dabb6361" +dependencies = [ + "lexical-util", + "lexical-write-integer", +] + +[[package]] +name = "lexical-write-integer" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "409851a618475d2d5796377cad353802345cba92c867d9fbcde9cf4eac4e14df" +dependencies = [ + "lexical-util", +] + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-keyutils" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "761e49ec5fd8a5a463f9b84e877c373d888935b71c6be78f3767fe2ae6bed18e" +dependencies = [ + "bitflags", + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "lru" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + +[[package]] +name = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework 3.7.0", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "oid-registry" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9" +dependencies = [ + "asn1-rs", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "openssl" +version = "0.10.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "opentelemetry" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaf416e4cb72756655126f7dd7bb0af49c674f4c1b9903e80c009e0c37e552e6" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "pin-project-lite", + "thiserror 2.0.18", + "tracing", +] + +[[package]] +name = "opentelemetry-http" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f6639e842a97dbea8886e3439710ae463120091e2e064518ba8e716e6ac36d" +dependencies = [ + "async-trait", + "bytes", + "http 1.4.0", + "opentelemetry", + "reqwest", +] + +[[package]] +name = "opentelemetry-otlp" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbee664a43e07615731afc539ca60c6d9f1a9425e25ca09c57bc36c87c55852b" +dependencies = [ + "http 1.4.0", + "opentelemetry", + "opentelemetry-http", + "opentelemetry-proto", + "opentelemetry_sdk", + "prost 0.13.5", + "reqwest", + "thiserror 2.0.18", + "tracing", +] + +[[package]] +name = "opentelemetry-proto" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e046fd7660710fe5a05e8748e70d9058dc15c94ba914e7c4faa7c728f0e8ddc" +dependencies = [ + "opentelemetry", + "opentelemetry_sdk", + "prost 0.13.5", + "tonic", +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d059a296a47436748557a353c5e6c5705b9470ef6c95cfc52c21a8814ddac2" + +[[package]] +name = "opentelemetry_sdk" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f644aa9e5e31d11896e024305d7e3c98a88884d9f8919dbf37a9991bc47a4b" +dependencies = [ + "futures-channel", + "futures-executor", + "futures-util", + "opentelemetry", + "percent-encoding", + "rand", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + +[[package]] +name = "p256" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" +dependencies = [ + "ecdsa", + "elliptic-curve", + "sha2", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap", +] + +[[package]] +name = "pin-project" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der 0.7.10", + "spki 0.7.3", +] + +[[package]] +name = "pkcs8" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" +dependencies = [ + "der 0.6.1", + "spki 0.6.0", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" +dependencies = [ + "bytes", + "prost-derive 0.12.6", +] + +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive 0.13.5", +] + +[[package]] +name = "prost" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ea70524a2f82d518bce41317d0fae74151505651af45faf1ffbd6fd33f0568" +dependencies = [ + "bytes", + "prost-derive 0.14.3", +] + +[[package]] +name = "prost-build" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7" +dependencies = [ + "heck 0.5.0", + "itertools 0.14.0", + "log", + "multimap", + "petgraph", + "prettyplease", + "prost 0.14.3", + "prost-types", + "regex", + "syn 2.0.117", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" +dependencies = [ + "anyhow", + "itertools 0.12.1", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "prost-derive" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "prost-types" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7" +dependencies = [ + "prost 0.14.3", +] + +[[package]] +name = "proto_generator" +version = "0.1.0" +source = "git+https://github.com/snowflakedb/universal-driver#080422e05fbd727f68d7c494e564ec625e1375d6" +dependencies = [ + "clap", + "env_logger", + "log", + "prost 0.12.6", + "prost-build", + "serde", + "serde_json", + "snafu 0.7.5", + "tempfile", + "walkdir", +] + +[[package]] +name = "proto_utils" +version = "0.1.0" +source = "git+https://github.com/snowflakedb/universal-driver#080422e05fbd727f68d7c494e564ec625e1375d6" + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls 0.23.37", + "socket2 0.6.3", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand", + "ring", + "rustc-hash", + "rustls 0.23.37", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.6.3", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-lite" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.4.13", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.8.1", + "hyper-rustls 0.27.7", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls 0.23.37", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tokio-rustls 0.26.4", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + +[[package]] +name = "rfc6979" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" +dependencies = [ + "crypto-bigint 0.4.9", + "hmac", + "zeroize", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted 0.9.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.7", + "sct", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki 0.103.9", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework 3.7.0", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted 0.9.0", +] + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted 0.9.0", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted 0.9.0", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted 0.9.0", +] + +[[package]] +name = "sec1" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" +dependencies = [ + "base16ct", + "der 0.6.1", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sf_core" +version = "0.0.0" +source = "git+https://github.com/snowflakedb/universal-driver#080422e05fbd727f68d7c494e564ec625e1375d6" +dependencies = [ + "arrow", + "arrow-ipc", + "aws-config", + "aws-credential-types", + "aws-lc-rs", + "aws-sdk-s3", + "base64 0.22.1", + "chrono", + "clap", + "const-oid", + "der 0.7.10", + "der-parser", + "dirs", + "error_trace", + "flate2", + "glob", + "hex", + "html-escape", + "infer", + "jwt", + "keyring", + "libc", + "lru 0.12.5", + "num-traits", + "once_cell", + "openssl", + "opentelemetry", + "opentelemetry-otlp", + "opentelemetry-semantic-conventions", + "opentelemetry_sdk", + "pkcs1", + "prost 0.14.3", + "proto_generator", + "proto_utils", + "rand", + "reqwest", + "rustls 0.23.37", + "rustls-native-certs", + "rustls-pemfile", + "rustls-webpki 0.102.8", + "serde", + "serde_json", + "sha2", + "signature 2.2.0", + "snafu 0.8.9", + "spki 0.7.3", + "thiserror 1.0.69", + "time", + "tokio", + "tokio-stream", + "tokio-util", + "toml", + "tracing", + "tracing-opentelemetry", + "tracing-subscriber", + "url", + "urlencoding", + "uuid", + "x509-cert", + "x509-parser", + "zeroize", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "snafu" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4de37ad025c587a29e8f3f5605c00f70b98715ef90b9061a815b9e59e9042d6" +dependencies = [ + "doc-comment", + "snafu-derive 0.7.5", +] + +[[package]] +name = "snafu" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e84b3f4eacbf3a1ce05eac6763b4d629d60cbc94d632e4092c54ade71f1e1a2" +dependencies = [ + "snafu-derive 0.8.9", +] + +[[package]] +name = "snafu-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990079665f075b699031e9c08fd3ab99be5029b96f3b78dc0709e8f77e4efebf" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "snafu-derive" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1c97747dbf44bb1ca44a561ece23508e99cb592e862f22222dcf42f51d1e451" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" + +[[package]] +name = "spki" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +dependencies = [ + "base64ct", + "der 0.6.1", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der 0.7.10", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tls_codec" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de2e01245e2bb89d6f05801c564fa27624dbd7b1846859876c7dad82e90bf6b" +dependencies = [ + "tls_codec_derive", + "zeroize", +] + +[[package]] +name = "tls_codec_derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d2e76690929402faae40aebdda620a2c0e25dd6d3b9afe48867dfd95991f4bd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tokio" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.3", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls 0.23.37", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "slab", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tonic" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e581ba15a835f4d9ea06c55ab1bd4dce26fc53752c69a04aac00703bfb49ba9" +dependencies = [ + "async-trait", + "base64 0.22.1", + "bytes", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "percent-encoding", + "pin-project", + "prost 0.13.5", + "tokio-stream", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-opentelemetry" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddcf5959f39507d0d04d6413119c04f33b623f4f951ebcbdddddfad2d0623a9c" +dependencies = [ + "js-sys", + "once_cell", + "opentelemetry", + "opentelemetry_sdk", + "smallvec", + "tracing", + "tracing-core", + "tracing-log", + "tracing-subscriber", + "web-time", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf8-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1292c0d970b54115d14f2492fe0170adf21d68a1de108eebc51c1df4f346a091" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "x509-cert" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" +dependencies = [ + "const-oid", + "der 0.7.10", + "spki 0.7.3", + "tls_codec", +] + +[[package]] +name = "x509-parser" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5030500cb2d66bdfbb4ebc9563be6ce7005a4b5d0f26be0c523870fe372ca6" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5f86989a046a79640b9d8867c823349a139367bda96549794fcc3313ce91f4e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/rust/adbc-snowflake/Cargo.toml b/rust/adbc-snowflake/Cargo.toml index 91cbbd3..9a4257c 100644 --- a/rust/adbc-snowflake/Cargo.toml +++ b/rust/adbc-snowflake/Cargo.toml @@ -10,6 +10,6 @@ crate-type = ["cdylib", "rlib"] adbc_core = "0.22.0" adbc_ffi = "0.22.0" sf_core = { git = "https://github.com/snowflakedb/universal-driver", subdirectory = "sf_core" } -arrow-array = { version = ">=53.1.0, <59", default-features = false, features = ["ffi"] } -arrow-schema = { version = ">=53.1.0, <59", default-features = false } +arrow-array = { version = ">=57.0.0, <58", default-features = false, features = ["ffi"] } +arrow-schema = { version = ">=57.0.0, <58", default-features = false } tokio = { version = "1", features = ["rt-multi-thread"] } diff --git a/rust/adbc-snowflake/src/connection.rs b/rust/adbc-snowflake/src/connection.rs new file mode 100644 index 0000000..559299a --- /dev/null +++ b/rust/adbc-snowflake/src/connection.rs @@ -0,0 +1,40 @@ +// src/connection.rs (stub) +use std::collections::HashSet; +use std::sync::Arc; +use adbc_core::{error::Result, options::{InfoCode, ObjectDepth, OptionConnection, OptionValue}, Optionable}; +use arrow_array::RecordBatchReader; +use arrow_schema::Schema; +use sf_core::apis::database_driver_v1::Handle; +use crate::driver::Inner; +use crate::statement::Statement; + +pub struct Connection { + pub(crate) inner: Arc, + pub(crate) conn_handle: Handle, + pub(crate) autocommit: bool, + pub(crate) active_transaction: bool, +} + +impl Optionable for Connection { + type Option = OptionConnection; + fn set_option(&mut self, _key: Self::Option, _value: OptionValue) -> Result<()> { todo!() } + fn get_option_string(&self, _key: Self::Option) -> Result { todo!() } + fn get_option_bytes(&self, _key: Self::Option) -> Result> { todo!() } + fn get_option_int(&self, _key: Self::Option) -> Result { todo!() } + fn get_option_double(&self, _key: Self::Option) -> Result { todo!() } +} + +impl adbc_core::Connection for Connection { + type StatementType = Statement; + fn new_statement(&mut self) -> Result { todo!() } + fn cancel(&mut self) -> Result<()> { Err(crate::error::not_implemented("cancel")) } + fn get_info(&self, _codes: Option>) -> Result> { todo!() } + fn get_objects(&self, _depth: ObjectDepth, _catalog: Option<&str>, _db_schema: Option<&str>, _table_name: Option<&str>, _table_type: Option>, _column_name: Option<&str>) -> Result> { Err(crate::error::not_implemented("get_objects")) } + fn get_table_schema(&self, _catalog: Option<&str>, _db_schema: Option<&str>, _table_name: &str) -> Result { todo!() } + fn get_table_types(&self) -> Result> { todo!() } + fn get_statistic_names(&self) -> Result> { Err(crate::error::not_implemented("get_statistic_names")) } + fn get_statistics(&self, _catalog: Option<&str>, _db_schema: Option<&str>, _table_name: Option<&str>, _approximate: bool) -> Result> { Err(crate::error::not_implemented("get_statistics")) } + fn commit(&mut self) -> Result<()> { todo!() } + fn rollback(&mut self) -> Result<()> { todo!() } + fn read_partition(&self, _partition: impl AsRef<[u8]>) -> Result> { Err(crate::error::not_implemented("read_partition")) } +} diff --git a/rust/adbc-snowflake/src/database.rs b/rust/adbc-snowflake/src/database.rs new file mode 100644 index 0000000..fd5254a --- /dev/null +++ b/rust/adbc-snowflake/src/database.rs @@ -0,0 +1,29 @@ +// src/database.rs (stub) +use std::collections::HashMap; +use std::sync::Arc; +use adbc_core::{error::Result, options::{OptionConnection, OptionDatabase, OptionValue}, Optionable}; +use sf_core::apis::database_driver_v1::Handle; +use sf_core::config::settings::Setting; +use crate::connection::Connection; +use crate::driver::Inner; + +pub struct Database { + pub(crate) inner: Arc, + pub(crate) db_handle: Handle, + pub(crate) sf_settings: HashMap, +} + +impl Optionable for Database { + type Option = OptionDatabase; + fn set_option(&mut self, _key: Self::Option, _value: OptionValue) -> Result<()> { todo!() } + fn get_option_string(&self, _key: Self::Option) -> Result { todo!() } + fn get_option_bytes(&self, _key: Self::Option) -> Result> { todo!() } + fn get_option_int(&self, _key: Self::Option) -> Result { todo!() } + fn get_option_double(&self, _key: Self::Option) -> Result { todo!() } +} + +impl adbc_core::Database for Database { + type ConnectionType = Connection; + fn new_connection(&self) -> Result { todo!() } + fn new_connection_with_opts(&self, _opts: impl IntoIterator) -> Result { todo!() } +} diff --git a/rust/adbc-snowflake/src/driver.rs b/rust/adbc-snowflake/src/driver.rs new file mode 100644 index 0000000..279d36d --- /dev/null +++ b/rust/adbc-snowflake/src/driver.rs @@ -0,0 +1,86 @@ +// src/driver.rs +use std::sync::Arc; + +use adbc_core::{ + error::{Error, Result, Status}, + options::{OptionDatabase, OptionValue}, + Optionable, +}; +use sf_core::apis::database_driver_v1::DatabaseDriverV1; +use tokio::runtime::Runtime; + +use crate::database::Database; + +pub(crate) struct Inner { + pub runtime: Runtime, + pub sf: DatabaseDriverV1, +} + +impl Inner { + fn new() -> Result { + let runtime = Runtime::new().map_err(|e| { + Error::with_message_and_status( + format!("Failed to create tokio runtime: {e}"), + Status::IO, + ) + })?; + Ok(Self { + runtime, + sf: DatabaseDriverV1::new(), + }) + } +} + +/// Snowflake ADBC Driver. +pub struct Driver { + pub(crate) inner: Arc, +} + +impl Default for Driver { + fn default() -> Self { + Self { + inner: Arc::new(Inner::new().expect("failed to initialize driver")), + } + } +} + +impl adbc_core::Driver for Driver { + type DatabaseType = Database; + + fn new_database(&mut self) -> Result { + self.new_database_with_opts(std::iter::empty()) + } + + fn new_database_with_opts( + &mut self, + opts: impl IntoIterator, + ) -> Result { + let db_handle = self.inner.sf.database_new(); + let mut db = Database { + inner: self.inner.clone(), + db_handle, + sf_settings: Default::default(), + }; + for (key, value) in opts { + db.set_option(key, value)?; + } + Ok(db) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use adbc_core::Driver as _; + + #[test] + fn driver_default_creates_successfully() { + let _driver = Driver::default(); + } + + #[test] + fn new_database_succeeds_with_no_options() { + let mut driver = Driver::default(); + let _db = driver.new_database().expect("new_database failed"); + } +} diff --git a/rust/adbc-snowflake/src/error.rs b/rust/adbc-snowflake/src/error.rs index 3c7dfe3..aad2656 100644 --- a/rust/adbc-snowflake/src/error.rs +++ b/rust/adbc-snowflake/src/error.rs @@ -35,12 +35,11 @@ mod tests { #[test] fn invalid_argument_maps_to_invalid_arguments() { - use sf_core::apis::database_driver_v1::ApiError; - // Build an InvalidArgument error via the snafu builder pattern - let err: ApiError = sf_core::apis::database_driver_v1::error::InvalidArgumentSnafu { - argument: "test".to_string(), - } - .build(); + use sf_core::apis::database_driver_v1::{DatabaseDriverV1, Handle}; + // Releasing a non-existent handle produces an InvalidArgument error + let driver = DatabaseDriverV1::new(); + let bogus_handle = Handle { id: 999, magic: 0 }; + let err = driver.database_init(bogus_handle).unwrap_err(); let adbc_err = api_error_to_adbc_error(err); assert_eq!(adbc_err.status, Status::InvalidArguments); } diff --git a/rust/adbc-snowflake/src/lib.rs b/rust/adbc-snowflake/src/lib.rs index 001cb16..248936c 100644 --- a/rust/adbc-snowflake/src/lib.rs +++ b/rust/adbc-snowflake/src/lib.rs @@ -1,4 +1,14 @@ // src/lib.rs mod error; -pub fn placeholder() {} +mod driver; +pub use driver::Driver; + +mod database; +pub use database::Database; + +mod connection; +pub use connection::Connection; + +mod statement; +pub use statement::Statement; diff --git a/rust/adbc-snowflake/src/statement.rs b/rust/adbc-snowflake/src/statement.rs new file mode 100644 index 0000000..a96b9c3 --- /dev/null +++ b/rust/adbc-snowflake/src/statement.rs @@ -0,0 +1,47 @@ +// src/statement.rs (stub) +use std::sync::Arc; +use adbc_core::{error::Result, options::{OptionStatement, OptionValue}, Optionable, PartitionedResult}; +use arrow_array::{RecordBatch, RecordBatchReader}; +use arrow_schema::Schema; +use sf_core::apis::database_driver_v1::Handle; +use crate::driver::Inner; + +pub struct Statement { + pub(crate) inner: Arc, + pub(crate) stmt_handle: Handle, + pub(crate) query: Option, + pub(crate) target_table: Option, + pub(crate) ingest_mode: Option, + pub(crate) query_tag: Option, +} + +impl Optionable for Statement { + type Option = OptionStatement; + fn set_option(&mut self, _key: Self::Option, _value: OptionValue) -> Result<()> { todo!() } + fn get_option_string(&self, _key: Self::Option) -> Result { todo!() } + fn get_option_bytes(&self, _key: Self::Option) -> Result> { todo!() } + fn get_option_int(&self, _key: Self::Option) -> Result { todo!() } + fn get_option_double(&self, _key: Self::Option) -> Result { todo!() } +} + +impl adbc_core::Statement for Statement { + fn bind(&mut self, _batch: RecordBatch) -> Result<()> { Err(crate::error::not_implemented("bind")) } + fn bind_stream(&mut self, _reader: Box) -> Result<()> { Err(crate::error::not_implemented("bind_stream")) } + fn execute(&mut self) -> Result> { todo!() } + fn execute_update(&mut self) -> Result> { todo!() } + fn execute_schema(&mut self) -> Result { Err(crate::error::not_implemented("execute_schema")) } + fn execute_partitions(&mut self) -> Result { Err(crate::error::not_implemented("execute_partitions")) } + fn get_parameter_schema(&self) -> Result { Err(crate::error::not_implemented("get_parameter_schema")) } + fn prepare(&mut self) -> Result<()> { + if self.query.is_none() { + return Err(adbc_core::error::Error::with_message_and_status( + "cannot prepare statement with no query", + adbc_core::error::Status::InvalidState, + )); + } + Ok(()) + } + fn set_sql_query(&mut self, query: impl AsRef) -> Result<()> { todo!() } + fn set_substrait_plan(&mut self, _plan: impl AsRef<[u8]>) -> Result<()> { Err(crate::error::not_implemented("set_substrait_plan")) } + fn cancel(&mut self) -> Result<()> { Err(crate::error::not_implemented("cancel")) } +} From 90399efccd0c2a291336c1d27ced7e9208d5a52b Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Thu, 19 Mar 2026 14:09:10 -0400 Subject: [PATCH 05/76] fix(rust): revert arrow version constraint to >=53.1.0, <59 --- rust/adbc-snowflake/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rust/adbc-snowflake/Cargo.toml b/rust/adbc-snowflake/Cargo.toml index 9a4257c..91cbbd3 100644 --- a/rust/adbc-snowflake/Cargo.toml +++ b/rust/adbc-snowflake/Cargo.toml @@ -10,6 +10,6 @@ crate-type = ["cdylib", "rlib"] adbc_core = "0.22.0" adbc_ffi = "0.22.0" sf_core = { git = "https://github.com/snowflakedb/universal-driver", subdirectory = "sf_core" } -arrow-array = { version = ">=57.0.0, <58", default-features = false, features = ["ffi"] } -arrow-schema = { version = ">=57.0.0, <58", default-features = false } +arrow-array = { version = ">=53.1.0, <59", default-features = false, features = ["ffi"] } +arrow-schema = { version = ">=53.1.0, <59", default-features = false } tokio = { version = "1", features = ["rt-multi-thread"] } From 4b480b0bc63509f066cfd741c0eff922c9f995e2 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Thu, 19 Mar 2026 14:20:15 -0400 Subject: [PATCH 06/76] feat(rust): implement Database option mapping and connection creation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the database.rs stub with full Optionable + adbc_core::Database implementation: ADBC key → sf_core param mapping, URI parsing, and new_connection_with_opts that propagates settings and calls connection_init. Adds set_autocommit/execute_simple stubs to connection.rs for compilation. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- rust/adbc-snowflake/src/connection.rs | 10 + rust/adbc-snowflake/src/database.rs | 429 +++++++++++++++++++++++++- 2 files changed, 430 insertions(+), 9 deletions(-) diff --git a/rust/adbc-snowflake/src/connection.rs b/rust/adbc-snowflake/src/connection.rs index 559299a..6ad7ef9 100644 --- a/rust/adbc-snowflake/src/connection.rs +++ b/rust/adbc-snowflake/src/connection.rs @@ -24,6 +24,16 @@ impl Optionable for Connection { fn get_option_double(&self, _key: Self::Option) -> Result { todo!() } } +impl Connection { + pub(crate) fn set_autocommit(&mut self, _enabled: bool) -> adbc_core::error::Result<()> { + Ok(()) // stub — full impl in Task 5 + } + + pub(crate) fn execute_simple(&self, _sql: &str) -> adbc_core::error::Result<()> { + Ok(()) // stub — full impl in Task 5 + } +} + impl adbc_core::Connection for Connection { type StatementType = Statement; fn new_statement(&mut self) -> Result { todo!() } diff --git a/rust/adbc-snowflake/src/database.rs b/rust/adbc-snowflake/src/database.rs index fd5254a..56dbb99 100644 --- a/rust/adbc-snowflake/src/database.rs +++ b/rust/adbc-snowflake/src/database.rs @@ -1,29 +1,440 @@ -// src/database.rs (stub) +// src/database.rs use std::collections::HashMap; use std::sync::Arc; -use adbc_core::{error::Result, options::{OptionConnection, OptionDatabase, OptionValue}, Optionable}; + +use adbc_core::{ + error::{Error, Result, Status}, + options::{OptionConnection, OptionDatabase, OptionValue}, + Optionable, +}; use sf_core::apis::database_driver_v1::Handle; use sf_core::config::settings::Setting; + use crate::connection::Connection; use crate::driver::Inner; +/// Convert an ADBC OptionDatabase key + OptionValue into an sf_core (param_name, Setting) pair. +/// Returns None for the "uri" key (handled by apply_uri separately). +/// Returns Err for keys with invalid values (e.g. non-numeric port). +fn adbc_db_opt_to_sf(key: &str, value: &OptionValue) -> Result> { + let setting = match value { + OptionValue::String(s) => Setting::String(s.clone()), + OptionValue::Int(i) => Setting::Int(*i), + OptionValue::Double(d) => Setting::Double(*d), + OptionValue::Bytes(b) => Setting::Bytes(b.clone()), + _ => { + return Err(Error::with_message_and_status( + "unsupported option value type", + Status::InvalidArguments, + )) + } + }; + + let param: String = match key { + "username" => "user".to_string(), + "password" => "password".to_string(), + "adbc.snowflake.sql.account" => "account".to_string(), + "adbc.snowflake.sql.db" => "database".to_string(), + "adbc.snowflake.sql.schema" => "schema".to_string(), + "adbc.snowflake.sql.warehouse" => "warehouse".to_string(), + "adbc.snowflake.sql.role" => "role".to_string(), + "adbc.snowflake.sql.uri.host" => "host".to_string(), + "adbc.snowflake.sql.uri.protocol" => "protocol".to_string(), + "adbc.snowflake.sql.auth_type" => "authenticator".to_string(), + "adbc.snowflake.sql.client_option.auth_token" => "token".to_string(), + "adbc.snowflake.sql.client_option.jwt_private_key" => "private_key_file".to_string(), + "adbc.snowflake.sql.client_option.jwt_private_key_pkcs8_value" => { + "private_key".to_string() + } + "adbc.snowflake.sql.client_option.jwt_private_key_pkcs8_password" => { + "private_key_password".to_string() + } + "adbc.snowflake.sql.uri.port" => { + let port = match value { + OptionValue::String(s) => s.parse::().map_err(|_| { + Error::with_message_and_status( + format!("invalid port value: {s}"), + Status::InvalidArguments, + ) + })?, + OptionValue::Int(i) => *i, + _ => { + return Err(Error::with_message_and_status( + "port must be a string or int", + Status::InvalidArguments, + )) + } + }; + return Ok(Some(("port".to_string(), Setting::Int(port)))); + } + "uri" => return Ok(None), + other => other.to_string(), + }; + + Ok(Some((param, setting))) +} + pub struct Database { pub(crate) inner: Arc, pub(crate) db_handle: Handle, + /// Local copy of sf_core settings keyed by canonical param name. + /// Propagated to each new connection before connection_init. pub(crate) sf_settings: HashMap, } +impl Drop for Database { + fn drop(&mut self) { + let _ = self.inner.sf.database_release(self.db_handle); + } +} + impl Optionable for Database { type Option = OptionDatabase; - fn set_option(&mut self, _key: Self::Option, _value: OptionValue) -> Result<()> { todo!() } - fn get_option_string(&self, _key: Self::Option) -> Result { todo!() } - fn get_option_bytes(&self, _key: Self::Option) -> Result> { todo!() } - fn get_option_int(&self, _key: Self::Option) -> Result { todo!() } - fn get_option_double(&self, _key: Self::Option) -> Result { todo!() } + + fn set_option(&mut self, key: Self::Option, value: OptionValue) -> Result<()> { + let key_str = key.as_ref(); + if key_str == "uri" { + if let OptionValue::String(uri) = &value { + return self.apply_uri(uri.clone()); + } + return Err(Error::with_message_and_status( + "uri option must be a string", + Status::InvalidArguments, + )); + } + if let Some((param, setting)) = adbc_db_opt_to_sf(key_str, &value)? { + self.sf_settings.insert(param.clone(), setting.clone()); + self.inner + .runtime + .block_on(self.inner.sf.database_set_option( + self.db_handle, + param, + setting, + )) + .map_err(crate::error::api_error_to_adbc_error)?; + } + Ok(()) + } + + fn get_option_string(&self, key: Self::Option) -> Result { + let key_str = key.as_ref(); + if let Ok(Some((param, _))) = + adbc_db_opt_to_sf(key_str, &OptionValue::String(String::new())) + { + if let Some(Setting::String(s)) = self.sf_settings.get(¶m) { + return Ok(s.clone()); + } + } + Err(Error::with_message_and_status( + format!("option not found: {key_str}"), + Status::NotFound, + )) + } + + fn get_option_bytes(&self, key: Self::Option) -> Result> { + let key_str = key.as_ref(); + if let Ok(Some((param, _))) = adbc_db_opt_to_sf(key_str, &OptionValue::Bytes(vec![])) { + if let Some(Setting::Bytes(b)) = self.sf_settings.get(¶m) { + return Ok(b.clone()); + } + } + Err(Error::with_message_and_status( + format!("option not found: {key_str}"), + Status::NotFound, + )) + } + + fn get_option_int(&self, key: Self::Option) -> Result { + let key_str = key.as_ref(); + if let Ok(Some((param, _))) = adbc_db_opt_to_sf(key_str, &OptionValue::Int(0)) { + if let Some(Setting::Int(i)) = self.sf_settings.get(¶m) { + return Ok(*i); + } + } + Err(Error::with_message_and_status( + format!("option not found: {key_str}"), + Status::NotFound, + )) + } + + fn get_option_double(&self, key: Self::Option) -> Result { + let key_str = key.as_ref(); + if let Ok(Some((param, _))) = adbc_db_opt_to_sf(key_str, &OptionValue::Double(0.0)) { + if let Some(Setting::Double(d)) = self.sf_settings.get(¶m) { + return Ok(*d); + } + } + Err(Error::with_message_and_status( + format!("option not found: {key_str}"), + Status::NotFound, + )) + } +} + +impl Database { + /// Parse a Snowflake URI and apply each component as an individual option. + /// Format: snowflake://[user[:password]@]account[/database[/schema]][?param=value&...] + /// Recognized query params: warehouse, role, host, port, protocol, authenticator + fn apply_uri(&mut self, uri: String) -> Result<()> { + let stripped = uri + .strip_prefix("snowflake://") + .unwrap_or(&uri) + .to_string(); + + let (user_info, rest) = if let Some(at) = stripped.find('@') { + ( + Some(stripped[..at].to_string()), + stripped[at + 1..].to_string(), + ) + } else { + (None, stripped) + }; + + if let Some(info) = user_info { + if let Some(colon) = info.find(':') { + let user = &info[..colon]; + let pass = &info[colon + 1..]; + if !user.is_empty() { + self.set_option( + OptionDatabase::Username, + OptionValue::String(user.to_string()), + )?; + } + self.set_option( + OptionDatabase::Password, + OptionValue::String(pass.to_string()), + )?; + } else if !info.is_empty() { + self.set_option(OptionDatabase::Username, OptionValue::String(info))?; + } + } + + let (path, query) = if let Some(q) = rest.find('?') { + (rest[..q].to_string(), Some(rest[q + 1..].to_string())) + } else { + (rest, None) + }; + + let parts: Vec<&str> = path.splitn(3, '/').collect(); + if let Some(account) = parts.first().filter(|s| !s.is_empty()) { + self.set_option( + OptionDatabase::Other("adbc.snowflake.sql.account".into()), + OptionValue::String(account.to_string()), + )?; + } + if let Some(database) = parts.get(1).filter(|s| !s.is_empty()) { + self.set_option( + OptionDatabase::Other("adbc.snowflake.sql.db".into()), + OptionValue::String(database.to_string()), + )?; + } + if let Some(schema) = parts.get(2).filter(|s| !s.is_empty()) { + self.set_option( + OptionDatabase::Other("adbc.snowflake.sql.schema".into()), + OptionValue::String(schema.to_string()), + )?; + } + + if let Some(q) = query { + for pair in q.split('&') { + if let Some(eq) = pair.find('=') { + let k = &pair[..eq]; + let v = &pair[eq + 1..]; + let adbc_key = match k { + "warehouse" => "adbc.snowflake.sql.warehouse", + "role" => "adbc.snowflake.sql.role", + "host" => "adbc.snowflake.sql.uri.host", + "port" => "adbc.snowflake.sql.uri.port", + "protocol" => "adbc.snowflake.sql.uri.protocol", + "authenticator" => "adbc.snowflake.sql.auth_type", + _ => continue, + }; + self.set_option( + OptionDatabase::Other(adbc_key.into()), + OptionValue::String(v.to_string()), + )?; + } + } + } + Ok(()) + } } impl adbc_core::Database for Database { type ConnectionType = Connection; - fn new_connection(&self) -> Result { todo!() } - fn new_connection_with_opts(&self, _opts: impl IntoIterator) -> Result { todo!() } + + fn new_connection(&self) -> Result { + self.new_connection_with_opts(std::iter::empty()) + } + + fn new_connection_with_opts( + &self, + opts: impl IntoIterator, + ) -> Result { + let conn_handle = self.inner.sf.connection_new(); + + // Propagate all database-level settings to the connection + for (param, setting) in &self.sf_settings { + self.inner + .runtime + .block_on(self.inner.sf.connection_set_option( + conn_handle, + param.clone(), + setting.clone(), + )) + .map_err(crate::error::api_error_to_adbc_error)?; + } + + let mut post_autocommit: Option = None; + let mut post_catalog: Option = None; + let mut post_schema: Option = None; + + for (key, value) in opts { + match &key { + OptionConnection::AutoCommit => { + if let OptionValue::String(s) = &value { + post_autocommit = Some(s == "true" || s == "1"); + } + } + OptionConnection::CurrentCatalog => { + if let OptionValue::String(s) = &value { + post_catalog = Some(s.clone()); + } + } + OptionConnection::CurrentSchema => { + if let OptionValue::String(s) = &value { + post_schema = Some(s.clone()); + } + } + OptionConnection::Other(k) => { + let sf_setting = match &value { + OptionValue::String(s) => Setting::String(s.clone()), + OptionValue::Int(i) => Setting::Int(*i), + OptionValue::Double(d) => Setting::Double(*d), + OptionValue::Bytes(b) => Setting::Bytes(b.clone()), + _ => { + return Err(Error::with_message_and_status( + "unsupported option value type", + Status::InvalidArguments, + )) + } + }; + self.inner + .runtime + .block_on(self.inner.sf.connection_set_option( + conn_handle, + k.clone(), + sf_setting, + )) + .map_err(crate::error::api_error_to_adbc_error)?; + } + _ => {} + } + } + + // Authenticate + self.inner + .runtime + .block_on(self.inner.sf.connection_init(conn_handle, self.db_handle)) + .map_err(crate::error::api_error_to_adbc_error)?; + + let mut conn = Connection { + inner: self.inner.clone(), + conn_handle, + autocommit: true, + active_transaction: false, + }; + + if let Some(ac) = post_autocommit { + conn.set_autocommit(ac)?; + } + if let Some(cat) = post_catalog { + conn.execute_simple(&format!( + r#"USE DATABASE "{}""#, + cat.replace('"', "\"\"") + ))?; + } + if let Some(sch) = post_schema { + conn.execute_simple(&format!( + r#"USE SCHEMA "{}""#, + sch.replace('"', "\"\"") + ))?; + } + + Ok(conn) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use adbc_core::{options::{OptionDatabase, OptionValue}, Driver as _}; + + fn make_db() -> Database { + let mut driver = crate::driver::Driver::default(); + driver.new_database().unwrap() + } + + #[test] + fn set_and_get_account_option() { + let mut db = make_db(); + db.set_option( + OptionDatabase::Other("adbc.snowflake.sql.account".into()), + OptionValue::String("myaccount".into()), + ) + .unwrap(); + assert_eq!( + db.get_option_string(OptionDatabase::Other( + "adbc.snowflake.sql.account".into() + )) + .unwrap(), + "myaccount" + ); + } + + #[test] + fn set_port_option_as_string_converts_to_int() { + let mut db = make_db(); + db.set_option( + OptionDatabase::Other("adbc.snowflake.sql.uri.port".into()), + OptionValue::String("443".into()), + ) + .unwrap(); + let setting = db.sf_settings.get("port").unwrap(); + assert_eq!(*setting, sf_core::config::settings::Setting::Int(443)); + } + + #[test] + fn username_maps_to_user_param() { + let mut db = make_db(); + db.set_option(OptionDatabase::Username, OptionValue::String("alice".into())) + .unwrap(); + let setting = db.sf_settings.get("user").unwrap(); + assert_eq!( + *setting, + sf_core::config::settings::Setting::String("alice".into()) + ); + } + + #[test] + fn uri_parses_account_user_database() { + let mut db = make_db(); + db.set_option( + OptionDatabase::Uri, + OptionValue::String("snowflake://alice:secret@myaccount/mydb/myschema".into()), + ) + .unwrap(); + assert_eq!( + db.sf_settings.get("account").unwrap(), + &sf_core::config::settings::Setting::String("myaccount".into()) + ); + assert_eq!( + db.sf_settings.get("user").unwrap(), + &sf_core::config::settings::Setting::String("alice".into()) + ); + assert_eq!( + db.sf_settings.get("database").unwrap(), + &sf_core::config::settings::Setting::String("mydb".into()) + ); + } } From 5fb4e95e504a24ef8e56a54e119caa8c559a5ac2 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Thu, 19 Mar 2026 14:31:37 -0400 Subject: [PATCH 07/76] docs(rust): document URI parsing limitations in apply_uri --- rust/adbc-snowflake/src/database.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/rust/adbc-snowflake/src/database.rs b/rust/adbc-snowflake/src/database.rs index 56dbb99..fc8bf41 100644 --- a/rust/adbc-snowflake/src/database.rs +++ b/rust/adbc-snowflake/src/database.rs @@ -175,6 +175,10 @@ impl Database { /// Parse a Snowflake URI and apply each component as an individual option. /// Format: snowflake://[user[:password]@]account[/database[/schema]][?param=value&...] /// Recognized query params: warehouse, role, host, port, protocol, authenticator + /// + /// Limitations: passwords containing `@` are not supported; use `set_option` for + /// Username/Password directly when credentials contain special characters. + /// Query parameter values are not URL-decoded. fn apply_uri(&mut self, uri: String) -> Result<()> { let stripped = uri .strip_prefix("snowflake://") From 194eff9bc170d311b05c401b857fe889a48b8b9e Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Thu, 19 Mar 2026 15:22:39 -0400 Subject: [PATCH 08/76] feat(rust): implement Connection with metadata and transaction support Implements full connection.rs for Task 5: lifecycle (Drop), set_autocommit/execute_simple helpers, Optionable (AutoCommit, CurrentCatalog, CurrentSchema), and adbc_core::Connection methods (new_statement, get_info, get_table_types, get_table_schema, commit, rollback). Adds snowflake_type_to_arrow mapping for DESC TABLE results. Also adds arrow-buffer v57 as a direct dep to resolve version conflict with arrow-array v57 transitive dep. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- rust/adbc-snowflake/Cargo.toml | 1 + rust/adbc-snowflake/src/connection.rs | 533 ++++++++++++++++++++++++-- 2 files changed, 508 insertions(+), 26 deletions(-) diff --git a/rust/adbc-snowflake/Cargo.toml b/rust/adbc-snowflake/Cargo.toml index 91cbbd3..0cb370c 100644 --- a/rust/adbc-snowflake/Cargo.toml +++ b/rust/adbc-snowflake/Cargo.toml @@ -11,5 +11,6 @@ adbc_core = "0.22.0" adbc_ffi = "0.22.0" sf_core = { git = "https://github.com/snowflakedb/universal-driver", subdirectory = "sf_core" } arrow-array = { version = ">=53.1.0, <59", default-features = false, features = ["ffi"] } +arrow-buffer = { version = "57", default-features = false } arrow-schema = { version = ">=53.1.0, <59", default-features = false } tokio = { version = "1", features = ["rt-multi-thread"] } diff --git a/rust/adbc-snowflake/src/connection.rs b/rust/adbc-snowflake/src/connection.rs index 6ad7ef9..9aa6a75 100644 --- a/rust/adbc-snowflake/src/connection.rs +++ b/rust/adbc-snowflake/src/connection.rs @@ -1,10 +1,21 @@ -// src/connection.rs (stub) +// src/connection.rs use std::collections::HashSet; use std::sync::Arc; -use adbc_core::{error::Result, options::{InfoCode, ObjectDepth, OptionConnection, OptionValue}, Optionable}; -use arrow_array::RecordBatchReader; -use arrow_schema::Schema; + +use adbc_core::{ + constants, + error::{Error, Result, Status}, + options::{InfoCode, ObjectDepth, OptionConnection, OptionValue}, + schemas, Optionable, +}; +use arrow_array::{ + ArrayRef, BooleanArray, Int64Array, RecordBatch, RecordBatchReader, StringArray, UInt32Array, + UnionArray, +}; +use arrow_buffer::ScalarBuffer; +use arrow_schema::{DataType, Field, Schema}; use sf_core::apis::database_driver_v1::Handle; + use crate::driver::Inner; use crate::statement::Statement; @@ -15,36 +26,506 @@ pub struct Connection { pub(crate) active_transaction: bool, } -impl Optionable for Connection { - type Option = OptionConnection; - fn set_option(&mut self, _key: Self::Option, _value: OptionValue) -> Result<()> { todo!() } - fn get_option_string(&self, _key: Self::Option) -> Result { todo!() } - fn get_option_bytes(&self, _key: Self::Option) -> Result> { todo!() } - fn get_option_int(&self, _key: Self::Option) -> Result { todo!() } - fn get_option_double(&self, _key: Self::Option) -> Result { todo!() } +impl Drop for Connection { + fn drop(&mut self) { + let _ = self.inner.sf.connection_release(self.conn_handle); + } +} + +struct SingleBatchReader { + batch: Option, + schema: std::sync::Arc, +} + +impl SingleBatchReader { + fn new(batch: RecordBatch) -> Self { + let schema = batch.schema(); + Self { batch: Some(batch), schema } + } +} + +impl Iterator for SingleBatchReader { + type Item = std::result::Result; + fn next(&mut self) -> Option { + Ok(self.batch.take()).transpose() + } +} + +impl RecordBatchReader for SingleBatchReader { + fn schema(&self) -> std::sync::Arc { + self.schema.clone() + } } impl Connection { - pub(crate) fn set_autocommit(&mut self, _enabled: bool) -> adbc_core::error::Result<()> { - Ok(()) // stub — full impl in Task 5 + pub(crate) fn execute_simple(&self, sql: &str) -> Result<()> { + let stmt_handle = self + .inner + .sf + .statement_new(self.conn_handle) + .map_err(crate::error::api_error_to_adbc_error)?; + let result = self.inner.runtime.block_on(async { + self.inner + .sf + .statement_set_sql_query(stmt_handle, sql.to_string()) + .await?; + self.inner + .sf + .statement_execute_query(stmt_handle, None) + .await + }); + let _ = self.inner.sf.statement_release(stmt_handle); + result + .map(|_| ()) + .map_err(crate::error::api_error_to_adbc_error) } - pub(crate) fn execute_simple(&self, _sql: &str) -> adbc_core::error::Result<()> { - Ok(()) // stub — full impl in Task 5 + pub(crate) fn set_autocommit(&mut self, enabled: bool) -> Result<()> { + if enabled { + if self.active_transaction { + self.execute_simple("COMMIT")?; + self.active_transaction = false; + } + self.execute_simple("ALTER SESSION SET AUTOCOMMIT = true")?; + self.autocommit = true; + } else { + self.execute_simple("ALTER SESSION SET AUTOCOMMIT = false")?; + if !self.active_transaction { + self.execute_simple("BEGIN")?; + self.active_transaction = true; + } + self.autocommit = false; + } + Ok(()) + } +} + +impl Optionable for Connection { + type Option = OptionConnection; + + fn set_option(&mut self, key: Self::Option, value: OptionValue) -> Result<()> { + match key { + OptionConnection::AutoCommit => { + let enabled = match &value { + OptionValue::String(s) => s == "true" || s == "1", + _ => { + return Err(Error::with_message_and_status( + "autocommit value must be a string", + Status::InvalidArguments, + )) + } + }; + self.set_autocommit(enabled) + } + OptionConnection::CurrentCatalog => { + if let OptionValue::String(s) = &value { + self.execute_simple(&format!( + r#"USE DATABASE "{}""#, + s.replace('"', "\"\"") + )) + } else { + Err(Error::with_message_and_status( + "current_catalog value must be a string", + Status::InvalidArguments, + )) + } + } + OptionConnection::CurrentSchema => { + if let OptionValue::String(s) = &value { + self.execute_simple(&format!( + r#"USE SCHEMA "{}""#, + s.replace('"', "\"\"") + )) + } else { + Err(Error::with_message_and_status( + "current_schema value must be a string", + Status::InvalidArguments, + )) + } + } + _ => Err(Error::with_message_and_status( + format!("unsupported connection option: {}", key.as_ref()), + Status::NotFound, + )), + } + } + + fn get_option_string(&self, key: Self::Option) -> Result { + Err(Error::with_message_and_status( + format!("option not found: {}", key.as_ref()), + Status::NotFound, + )) + } + + fn get_option_bytes(&self, _key: Self::Option) -> Result> { + Err(Error::with_message_and_status( + "option not found", + Status::NotFound, + )) + } + + fn get_option_int(&self, _key: Self::Option) -> Result { + Err(Error::with_message_and_status( + "option not found", + Status::NotFound, + )) + } + + fn get_option_double(&self, _key: Self::Option) -> Result { + Err(Error::with_message_and_status( + "option not found", + Status::NotFound, + )) } } impl adbc_core::Connection for Connection { type StatementType = Statement; - fn new_statement(&mut self) -> Result { todo!() } - fn cancel(&mut self) -> Result<()> { Err(crate::error::not_implemented("cancel")) } - fn get_info(&self, _codes: Option>) -> Result> { todo!() } - fn get_objects(&self, _depth: ObjectDepth, _catalog: Option<&str>, _db_schema: Option<&str>, _table_name: Option<&str>, _table_type: Option>, _column_name: Option<&str>) -> Result> { Err(crate::error::not_implemented("get_objects")) } - fn get_table_schema(&self, _catalog: Option<&str>, _db_schema: Option<&str>, _table_name: &str) -> Result { todo!() } - fn get_table_types(&self) -> Result> { todo!() } - fn get_statistic_names(&self) -> Result> { Err(crate::error::not_implemented("get_statistic_names")) } - fn get_statistics(&self, _catalog: Option<&str>, _db_schema: Option<&str>, _table_name: Option<&str>, _approximate: bool) -> Result> { Err(crate::error::not_implemented("get_statistics")) } - fn commit(&mut self) -> Result<()> { todo!() } - fn rollback(&mut self) -> Result<()> { todo!() } - fn read_partition(&self, _partition: impl AsRef<[u8]>) -> Result> { Err(crate::error::not_implemented("read_partition")) } + + fn new_statement(&mut self) -> Result { + let stmt_handle = self + .inner + .sf + .statement_new(self.conn_handle) + .map_err(crate::error::api_error_to_adbc_error)?; + Ok(Statement { + inner: self.inner.clone(), + stmt_handle, + query: None, + target_table: None, + ingest_mode: None, + query_tag: None, + }) + } + + fn cancel(&mut self) -> Result<()> { + Err(crate::error::not_implemented("cancel")) + } + + fn get_info( + &self, + codes: Option>, + ) -> Result> { + // (InfoCode, type_id, offset_within_arm_array) + let all_entries: &[(InfoCode, i8, i32)] = &[ + (InfoCode::VendorName, 0, 0), + (InfoCode::VendorSql, 1, 0), + (InfoCode::VendorSubstrait, 1, 1), + (InfoCode::DriverName, 0, 1), + (InfoCode::DriverVersion, 0, 2), + (InfoCode::DriverAdbcVersion, 2, 0), + ]; + + let selected: Vec<_> = match &codes { + None => all_entries.iter().collect(), + Some(set) => all_entries + .iter() + .filter(|(c, _, _)| set.contains(c)) + .collect(), + }; + + if selected.is_empty() { + let batch = RecordBatch::new_empty(schemas::GET_INFO_SCHEMA.clone()); + return Ok(Box::new(SingleBatchReader::new(batch))); + } + + let name_vals: Vec = selected + .iter() + .map(|(c, _, _)| u32::from(c)) + .collect(); + let type_ids: Vec = selected.iter().map(|(_, t, _)| *t).collect(); + let offsets: Vec = selected.iter().map(|(_, _, o)| *o).collect(); + + use arrow_schema::UnionFields; + + let string_values = Arc::new(StringArray::from(vec![ + "Snowflake", + "ADBC Snowflake Driver (Rust)", + env!("CARGO_PKG_VERSION"), + ])) as ArrayRef; + let bool_values = Arc::new(BooleanArray::from(vec![true, false])) as ArrayRef; + let int64_values = + Arc::new(Int64Array::from(vec![constants::ADBC_VERSION_1_1_0 as i64])) as ArrayRef; + let int32_values = + Arc::new(arrow_array::Int32Array::from(vec![0i32])) as ArrayRef; + let list_values = Arc::new(arrow_array::ListArray::new_null( + Arc::new(Field::new("item", DataType::Utf8, true)), + 0, + )) as ArrayRef; + let map_values = Arc::new(arrow_array::ListArray::new_null( + Arc::new(Field::new("item", DataType::Utf8, true)), + 0, + )) as ArrayRef; + + let union_array = UnionArray::try_new( + #[allow(deprecated)] + UnionFields::new( + [0i8, 1, 2, 3, 4, 5], + [ + Field::new("string_value", DataType::Utf8, true), + Field::new("bool_value", DataType::Boolean, true), + Field::new("int64_value", DataType::Int64, true), + Field::new("int32_bitmask", DataType::Int32, true), + Field::new_list( + "string_list", + Field::new_list_field(DataType::Utf8, true), + true, + ), + Field::new_list( + "int32_to_int32_list_map", + Field::new_list_field(DataType::Int32, true), + true, + ), + ], + ), + type_ids.into_iter().collect::>(), + Some(offsets.into_iter().collect::>()), + vec![ + string_values, + bool_values, + int64_values, + int32_values, + list_values, + map_values, + ], + ) + .map_err(|e| Error::with_message_and_status(e.to_string(), Status::Internal))?; + + let batch = RecordBatch::try_new( + schemas::GET_INFO_SCHEMA.clone(), + vec![ + Arc::new(UInt32Array::from(name_vals)) as ArrayRef, + Arc::new(union_array) as ArrayRef, + ], + ) + .map_err(|e| Error::with_message_and_status(e.to_string(), Status::Internal))?; + + Ok(Box::new(SingleBatchReader::new(batch))) + } + + fn get_objects( + &self, + _depth: ObjectDepth, + _catalog: Option<&str>, + _db_schema: Option<&str>, + _table_name: Option<&str>, + _table_type: Option>, + _column_name: Option<&str>, + ) -> Result> { + Err(crate::error::not_implemented("get_objects")) + } + + fn get_table_schema( + &self, + catalog: Option<&str>, + db_schema: Option<&str>, + table_name: &str, + ) -> Result { + let quoted = |s: &str| format!(r#""{}""#, s.replace('"', "\"\"")); + let qualified = match (catalog, db_schema) { + (Some(c), Some(s)) => { + format!("{}.{}.{}", quoted(c), quoted(s), quoted(table_name)) + } + (None, Some(s)) => format!("{}.{}", quoted(s), quoted(table_name)), + (Some(c), None) => format!("{}.{}", quoted(c), quoted(table_name)), + (None, None) => quoted(table_name), + }; + let sql = format!("DESC TABLE {qualified}"); + let stmt_handle = self + .inner + .sf + .statement_new(self.conn_handle) + .map_err(crate::error::api_error_to_adbc_error)?; + let result = self.inner.runtime.block_on(async { + self.inner + .sf + .statement_set_sql_query(stmt_handle, sql) + .await?; + self.inner + .sf + .statement_execute_query(stmt_handle, None) + .await + }); + let _ = self.inner.sf.statement_release(stmt_handle); + let exec_result = result.map_err(crate::error::api_error_to_adbc_error)?; + + let raw = Box::into_raw(exec_result.stream) + as *mut arrow_array::ffi_stream::FFI_ArrowArrayStream; + let mut reader = unsafe { arrow_array::ffi_stream::ArrowArrayStreamReader::from_raw(raw) } + .map_err(|e| Error::with_message_and_status(e.to_string(), Status::IO))?; + + let mut fields: Vec = Vec::new(); + while let Some(batch) = reader.next() { + let batch = + batch.map_err(|e| Error::with_message_and_status(e.to_string(), Status::IO))?; + if batch.num_columns() < 4 { + continue; + } + use arrow_array::cast::AsArray; + let names = batch.column(0).as_string::(); + let types = batch.column(1).as_string::(); + let nullables = batch.column(3).as_string::(); + for i in 0..batch.num_rows() { + let arrow_type = snowflake_type_to_arrow(types.value(i)); + fields.push(Field::new( + names.value(i), + arrow_type, + nullables.value(i) == "Y", + )); + } + } + Ok(Schema::new(fields)) + } + + fn get_table_types(&self) -> Result> { + let array = Arc::new(StringArray::from(vec!["TABLE", "VIEW"])); + let batch = + RecordBatch::try_new(schemas::GET_TABLE_TYPES_SCHEMA.clone(), vec![array]) + .map_err(|e| { + Error::with_message_and_status(e.to_string(), Status::Internal) + })?; + Ok(Box::new(SingleBatchReader::new(batch))) + } + + fn get_statistic_names(&self) -> Result> { + Err(crate::error::not_implemented("get_statistic_names")) + } + + fn get_statistics( + &self, + _catalog: Option<&str>, + _db_schema: Option<&str>, + _table_name: Option<&str>, + _approximate: bool, + ) -> Result> { + Err(crate::error::not_implemented("get_statistics")) + } + + fn commit(&mut self) -> Result<()> { + self.execute_simple("COMMIT")?; + self.execute_simple("BEGIN") + } + + fn rollback(&mut self) -> Result<()> { + self.execute_simple("ROLLBACK")?; + self.execute_simple("BEGIN") + } + + fn read_partition( + &self, + _partition: impl AsRef<[u8]>, + ) -> Result> { + Err(crate::error::not_implemented("read_partition")) + } +} + +fn snowflake_type_to_arrow(type_str: &str) -> DataType { + let upper = type_str.to_uppercase(); + let base = upper.split('(').next().unwrap_or(&upper).trim(); + match base { + "FLOAT" | "DOUBLE" | "REAL" | "FLOAT4" | "FLOAT8" => DataType::Float64, + "BOOLEAN" => DataType::Boolean, + "DATE" => DataType::Date32, + "TIME" => DataType::Time64(arrow_schema::TimeUnit::Nanosecond), + "TEXT" | "STRING" | "VARCHAR" | "CHAR" | "CHARACTER" | "NCHAR" | "NVARCHAR" + | "NVARCHAR2" | "CHAR VARYING" | "NCHAR VARYING" => DataType::Utf8, + "BINARY" | "VARBINARY" => DataType::Binary, + "ARRAY" | "OBJECT" | "VARIANT" | "GEOGRAPHY" | "GEOMETRY" => DataType::Utf8, + "NUMBER" | "NUMERIC" | "DECIMAL" | "INT" | "INTEGER" | "BIGINT" | "SMALLINT" + | "TINYINT" | "BYTEINT" => { + if let Some(inner) = type_str + .find('(') + .and_then(|s| type_str.rfind(')').map(|e| &type_str[s + 1..e])) + { + let scale = inner + .split(',') + .nth(1) + .and_then(|s| s.trim().parse::().ok()) + .unwrap_or(0); + if scale == 0 { + DataType::Int64 + } else { + DataType::Float64 + } + } else { + DataType::Int64 + } + } + "TIMESTAMP" | "TIMESTAMP_NTZ" | "DATETIME" => { + DataType::Timestamp(arrow_schema::TimeUnit::Nanosecond, None) + } + "TIMESTAMP_LTZ" | "TIMESTAMP_TZ" => { + DataType::Timestamp(arrow_schema::TimeUnit::Nanosecond, Some("UTC".into())) + } + _ => DataType::Utf8, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn get_option_string_returns_not_found_for_unknown_key() { + let driver = crate::driver::Driver::default(); + let conn = Connection { + inner: driver.inner.clone(), + conn_handle: sf_core::apis::database_driver_v1::Handle { id: 0, magic: 0 }, + autocommit: true, + active_transaction: false, + }; + let result = conn.get_option_string(OptionConnection::Other("unknown".into())); + assert_eq!(result.unwrap_err().status, Status::NotFound); + } + + #[test] + fn snowflake_type_number_no_scale_is_int64() { + assert_eq!(snowflake_type_to_arrow("NUMBER(38,0)"), DataType::Int64); + } + + #[test] + fn snowflake_type_number_with_scale_is_float64() { + assert_eq!(snowflake_type_to_arrow("NUMBER(10,2)"), DataType::Float64); + } + + #[test] + fn snowflake_type_text_is_utf8() { + assert_eq!(snowflake_type_to_arrow("TEXT"), DataType::Utf8); + assert_eq!(snowflake_type_to_arrow("VARCHAR(16777216)"), DataType::Utf8); + } + + #[test] + fn snowflake_type_boolean_is_boolean() { + assert_eq!(snowflake_type_to_arrow("BOOLEAN"), DataType::Boolean); + } + + #[test] + fn snowflake_type_timestamp_ntz_is_nanosecond() { + assert_eq!( + snowflake_type_to_arrow("TIMESTAMP_NTZ(9)"), + DataType::Timestamp(arrow_schema::TimeUnit::Nanosecond, None) + ); + } + + #[test] + fn get_table_types_returns_table_and_view() { + use adbc_core::Connection as _; + use arrow_array::cast::AsArray; + let driver = crate::driver::Driver::default(); + let conn = Connection { + inner: driver.inner.clone(), + conn_handle: sf_core::apis::database_driver_v1::Handle { id: 0, magic: 0 }, + autocommit: true, + active_transaction: false, + }; + let mut reader = conn.get_table_types().unwrap(); + let batch = reader.next().unwrap().unwrap(); + let types: Vec<&str> = batch + .column(0) + .as_string::() + .iter() + .filter_map(|v| v) + .collect(); + assert_eq!(types, vec!["TABLE", "VIEW"]); + } } From b8f0b10a24f609ab7952d68e02828db7bdadcc68 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Thu, 19 Mar 2026 15:28:36 -0400 Subject: [PATCH 09/76] fix(rust): align arrow-buffer version constraint with arrow-array --- rust/adbc-snowflake/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/adbc-snowflake/Cargo.toml b/rust/adbc-snowflake/Cargo.toml index 0cb370c..5f26683 100644 --- a/rust/adbc-snowflake/Cargo.toml +++ b/rust/adbc-snowflake/Cargo.toml @@ -11,6 +11,6 @@ adbc_core = "0.22.0" adbc_ffi = "0.22.0" sf_core = { git = "https://github.com/snowflakedb/universal-driver", subdirectory = "sf_core" } arrow-array = { version = ">=53.1.0, <59", default-features = false, features = ["ffi"] } -arrow-buffer = { version = "57", default-features = false } +arrow-buffer = { version = ">=53.1.0, <59", default-features = false } arrow-schema = { version = ">=53.1.0, <59", default-features = false } tokio = { version = "1", features = ["rt-multi-thread"] } From bc5b269d77b2a846c23f246008ca3b992cbfd3f8 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Thu, 19 Mar 2026 15:31:11 -0400 Subject: [PATCH 10/76] fix(rust): respect autocommit mode in commit/rollback; document FFI safety --- rust/adbc-snowflake/src/connection.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/rust/adbc-snowflake/src/connection.rs b/rust/adbc-snowflake/src/connection.rs index 9aa6a75..9a5a449 100644 --- a/rust/adbc-snowflake/src/connection.rs +++ b/rust/adbc-snowflake/src/connection.rs @@ -349,6 +349,9 @@ impl adbc_core::Connection for Connection { let _ = self.inner.sf.statement_release(stmt_handle); let exec_result = result.map_err(crate::error::api_error_to_adbc_error)?; + // Safety: exec_result.stream is a valid FFI stream from sf_core. We take ownership + // via Box::into_raw and transfer it to ArrowArrayStreamReader. The C ABI layout is + // stable across arrow versions per the Arrow C Data Interface specification. let raw = Box::into_raw(exec_result.stream) as *mut arrow_array::ffi_stream::FFI_ArrowArrayStream; let mut reader = unsafe { arrow_array::ffi_stream::ArrowArrayStreamReader::from_raw(raw) } @@ -403,12 +406,20 @@ impl adbc_core::Connection for Connection { fn commit(&mut self) -> Result<()> { self.execute_simple("COMMIT")?; - self.execute_simple("BEGIN") + if !self.autocommit { + self.execute_simple("BEGIN")?; + self.active_transaction = true; + } + Ok(()) } fn rollback(&mut self) -> Result<()> { self.execute_simple("ROLLBACK")?; - self.execute_simple("BEGIN") + if !self.autocommit { + self.execute_simple("BEGIN")?; + self.active_transaction = true; + } + Ok(()) } fn read_partition( From d6b29a204e6812e6c53b4f96c955d113f3dc7dfa Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Thu, 19 Mar 2026 15:33:54 -0400 Subject: [PATCH 11/76] feat(rust): implement Statement with SQL execution and option handling --- rust/adbc-snowflake/src/statement.rs | 269 +++++++++++++++++++++++++-- 1 file changed, 250 insertions(+), 19 deletions(-) diff --git a/rust/adbc-snowflake/src/statement.rs b/rust/adbc-snowflake/src/statement.rs index a96b9c3..0ae8bb1 100644 --- a/rust/adbc-snowflake/src/statement.rs +++ b/rust/adbc-snowflake/src/statement.rs @@ -1,9 +1,15 @@ -// src/statement.rs (stub) +// src/statement.rs use std::sync::Arc; -use adbc_core::{error::Result, options::{OptionStatement, OptionValue}, Optionable, PartitionedResult}; + +use adbc_core::{ + error::{Error, Result, Status}, + options::{OptionStatement, OptionValue}, + Optionable, PartitionedResult, +}; use arrow_array::{RecordBatch, RecordBatchReader}; use arrow_schema::Schema; use sf_core::apis::database_driver_v1::Handle; + use crate::driver::Inner; pub struct Statement { @@ -15,33 +21,258 @@ pub struct Statement { pub(crate) query_tag: Option, } +impl Drop for Statement { + fn drop(&mut self) { + let _ = self.inner.sf.statement_release(self.stmt_handle); + } +} + impl Optionable for Statement { type Option = OptionStatement; - fn set_option(&mut self, _key: Self::Option, _value: OptionValue) -> Result<()> { todo!() } - fn get_option_string(&self, _key: Self::Option) -> Result { todo!() } - fn get_option_bytes(&self, _key: Self::Option) -> Result> { todo!() } - fn get_option_int(&self, _key: Self::Option) -> Result { todo!() } - fn get_option_double(&self, _key: Self::Option) -> Result { todo!() } + + fn set_option(&mut self, key: Self::Option, value: OptionValue) -> Result<()> { + match key { + OptionStatement::TargetTable => { + if let OptionValue::String(s) = value { + self.query = None; + self.target_table = Some(s); + Ok(()) + } else { + Err(Error::with_message_and_status("target_table must be a string", Status::InvalidArguments)) + } + } + OptionStatement::IngestMode => { + if let OptionValue::String(s) = value { + self.ingest_mode = Some(s); + Ok(()) + } else { + Err(Error::with_message_and_status("ingest_mode must be a string", Status::InvalidArguments)) + } + } + OptionStatement::Other(ref k) if k == "adbc.snowflake.statement.query_tag" => { + if let OptionValue::String(s) = value { + self.query_tag = Some(s); + Ok(()) + } else { + Err(Error::with_message_and_status("query_tag must be a string", Status::InvalidArguments)) + } + } + _ => Err(Error::with_message_and_status( + format!("unknown statement option: {}", key.as_ref()), + Status::NotFound, + )), + } + } + + fn get_option_string(&self, key: Self::Option) -> Result { + match key { + OptionStatement::Other(ref k) if k == "adbc.snowflake.statement.query_tag" => { + Ok(self.query_tag.clone().unwrap_or_default()) + } + _ => Err(Error::with_message_and_status( + format!("option not found: {}", key.as_ref()), + Status::NotFound, + )), + } + } + + fn get_option_bytes(&self, _key: Self::Option) -> Result> { + Err(Error::with_message_and_status("option not found", Status::NotFound)) + } + + fn get_option_int(&self, _key: Self::Option) -> Result { + Err(Error::with_message_and_status("option not found", Status::NotFound)) + } + + fn get_option_double(&self, _key: Self::Option) -> Result { + Err(Error::with_message_and_status("option not found", Status::NotFound)) + } } impl adbc_core::Statement for Statement { - fn bind(&mut self, _batch: RecordBatch) -> Result<()> { Err(crate::error::not_implemented("bind")) } - fn bind_stream(&mut self, _reader: Box) -> Result<()> { Err(crate::error::not_implemented("bind_stream")) } - fn execute(&mut self) -> Result> { todo!() } - fn execute_update(&mut self) -> Result> { todo!() } - fn execute_schema(&mut self) -> Result { Err(crate::error::not_implemented("execute_schema")) } - fn execute_partitions(&mut self) -> Result { Err(crate::error::not_implemented("execute_partitions")) } - fn get_parameter_schema(&self) -> Result { Err(crate::error::not_implemented("get_parameter_schema")) } + fn bind(&mut self, _batch: RecordBatch) -> Result<()> { + Err(crate::error::not_implemented("bind")) + } + + fn bind_stream(&mut self, _reader: Box) -> Result<()> { + Err(crate::error::not_implemented("bind_stream")) + } + + fn execute(&mut self) -> Result> { + if self.target_table.is_some() { + return Err(crate::error::not_implemented( + "bulk ingestion (target_table) is not yet implemented", + )); + } + let query = self.query.clone().ok_or_else(|| { + Error::with_message_and_status("cannot execute without a query", Status::InvalidState) + })?; + + // NOTE: query_tag SET not yet implemented — conn_handle added in Task 8 + + let result = self.inner.runtime.block_on(async { + self.inner.sf.statement_set_sql_query(self.stmt_handle, query).await?; + self.inner.sf.statement_execute_query(self.stmt_handle, None).await + }).map_err(crate::error::api_error_to_adbc_error)?; + + // Safety: result.stream is a valid FFI stream from sf_core. Ownership is transferred + // to ArrowArrayStreamReader. The C ABI layout is stable per the Arrow C Data Interface. + let raw = Box::into_raw(result.stream) as *mut arrow_array::ffi_stream::FFI_ArrowArrayStream; + let reader = unsafe { arrow_array::ffi_stream::ArrowArrayStreamReader::from_raw(raw) } + .map_err(|e| Error::with_message_and_status(e.to_string(), Status::IO))?; + Ok(Box::new(reader)) + } + + fn execute_update(&mut self) -> Result> { + if self.target_table.is_some() { + return Err(crate::error::not_implemented( + "bulk ingestion (target_table) is not yet implemented", + )); + } + let query = self.query.clone().ok_or_else(|| { + Error::with_message_and_status("cannot execute without a query", Status::InvalidState) + })?; + + // NOTE: query_tag SET not yet implemented — conn_handle added in Task 8 + + let result = self.inner.runtime.block_on(async { + self.inner.sf.statement_set_sql_query(self.stmt_handle, query).await?; + self.inner.sf.statement_execute_query(self.stmt_handle, None).await + }).map_err(crate::error::api_error_to_adbc_error)?; + + Ok(result.rows_affected) + } + + fn execute_schema(&mut self) -> Result { + Err(crate::error::not_implemented("execute_schema")) + } + + fn execute_partitions(&mut self) -> Result { + Err(crate::error::not_implemented("execute_partitions")) + } + + fn get_parameter_schema(&self) -> Result { + Err(crate::error::not_implemented("get_parameter_schema")) + } + fn prepare(&mut self) -> Result<()> { if self.query.is_none() { - return Err(adbc_core::error::Error::with_message_and_status( + return Err(Error::with_message_and_status( "cannot prepare statement with no query", - adbc_core::error::Status::InvalidState, + Status::InvalidState, )); } + Ok(()) // No-op: Snowflake has no server-side prepare + } + + fn set_sql_query(&mut self, query: impl AsRef) -> Result<()> { + self.query = Some(query.as_ref().to_string()); + self.target_table = None; Ok(()) } - fn set_sql_query(&mut self, query: impl AsRef) -> Result<()> { todo!() } - fn set_substrait_plan(&mut self, _plan: impl AsRef<[u8]>) -> Result<()> { Err(crate::error::not_implemented("set_substrait_plan")) } - fn cancel(&mut self) -> Result<()> { Err(crate::error::not_implemented("cancel")) } + + fn set_substrait_plan(&mut self, _plan: impl AsRef<[u8]>) -> Result<()> { + Err(crate::error::not_implemented("Snowflake does not support Substrait plans")) + } + + fn cancel(&mut self) -> Result<()> { + Err(crate::error::not_implemented("cancel")) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use adbc_core::Statement as _; + + fn make_stmt() -> Statement { + let driver = crate::driver::Driver::default(); + Statement { + inner: driver.inner.clone(), + stmt_handle: sf_core::apis::database_driver_v1::Handle { id: 0, magic: 0 }, + query: None, + target_table: None, + ingest_mode: None, + query_tag: None, + } + } + + #[test] + fn set_sql_query_stores_query() { + let mut stmt = make_stmt(); + stmt.set_sql_query("SELECT 1").unwrap(); + assert_eq!(stmt.query.as_deref(), Some("SELECT 1")); + } + + #[test] + fn execute_without_query_returns_invalid_state() { + let mut stmt = make_stmt(); + match stmt.execute() { + Err(err) => assert_eq!(err.status, adbc_core::error::Status::InvalidState), + Ok(_) => panic!("execute should have returned an error"), + } + } + + #[test] + fn execute_with_target_table_returns_not_implemented() { + let driver = crate::driver::Driver::default(); + let mut stmt = Statement { + inner: driver.inner.clone(), + stmt_handle: sf_core::apis::database_driver_v1::Handle { id: 0, magic: 0 }, + query: None, + target_table: Some("mytable".into()), + ingest_mode: None, + query_tag: None, + }; + match stmt.execute() { + Err(err) => assert_eq!(err.status, adbc_core::error::Status::NotImplemented), + Ok(_) => panic!("execute should have returned an error"), + } + } + + #[test] + fn set_query_clears_target_table() { + let driver = crate::driver::Driver::default(); + let mut stmt = Statement { + inner: driver.inner.clone(), + stmt_handle: sf_core::apis::database_driver_v1::Handle { id: 0, magic: 0 }, + query: None, + target_table: Some("mytable".into()), + ingest_mode: None, + query_tag: None, + }; + stmt.set_sql_query("SELECT 1").unwrap(); + assert!(stmt.target_table.is_none()); + } + + #[test] + fn prepare_without_query_returns_invalid_state() { + let mut stmt = make_stmt(); + let err = stmt.prepare().unwrap_err(); + assert_eq!(err.status, adbc_core::error::Status::InvalidState); + } + + #[test] + fn prepare_with_query_is_noop() { + let mut stmt = make_stmt(); + stmt.set_sql_query("SELECT 1").unwrap(); + stmt.prepare().unwrap(); + } + + #[test] + fn set_target_table_option() { + let mut stmt = make_stmt(); + stmt.set_option(OptionStatement::TargetTable, OptionValue::String("mytable".into())).unwrap(); + assert_eq!(stmt.target_table.as_deref(), Some("mytable")); + } + + #[test] + fn unknown_option_returns_not_found() { + let mut stmt = make_stmt(); + let err = stmt.set_option( + OptionStatement::Other("unknown.option".into()), + OptionValue::String("val".into()), + ).unwrap_err(); + assert_eq!(err.status, adbc_core::error::Status::NotFound); + } } From 5b4c3039fa61fb62b39e6b35d0112b4e67b51b66 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Thu, 19 Mar 2026 15:47:15 -0400 Subject: [PATCH 12/76] feat(rust): wire export_driver! and complete adbc-snowflake driver --- rust/adbc-snowflake/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rust/adbc-snowflake/src/lib.rs b/rust/adbc-snowflake/src/lib.rs index 248936c..0d385b9 100644 --- a/rust/adbc-snowflake/src/lib.rs +++ b/rust/adbc-snowflake/src/lib.rs @@ -12,3 +12,5 @@ pub use connection::Connection; mod statement; pub use statement::Statement; + +adbc_ffi::export_driver!(AdbcDriverSnowflakeInit, Driver); From 7c83d8e75bd65cff33295d1237614ec3b019552b Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Thu, 19 Mar 2026 15:52:34 -0400 Subject: [PATCH 13/76] feat(rust): fix query_tag by adding conn_handle to Statement --- rust/adbc-snowflake/src/connection.rs | 1 + rust/adbc-snowflake/src/statement.rs | 47 +++++++++++++++++++++++++-- 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/rust/adbc-snowflake/src/connection.rs b/rust/adbc-snowflake/src/connection.rs index 9a5a449..4259ae2 100644 --- a/rust/adbc-snowflake/src/connection.rs +++ b/rust/adbc-snowflake/src/connection.rs @@ -191,6 +191,7 @@ impl adbc_core::Connection for Connection { Ok(Statement { inner: self.inner.clone(), stmt_handle, + conn_handle: self.conn_handle, query: None, target_table: None, ingest_mode: None, diff --git a/rust/adbc-snowflake/src/statement.rs b/rust/adbc-snowflake/src/statement.rs index 0ae8bb1..0a100f3 100644 --- a/rust/adbc-snowflake/src/statement.rs +++ b/rust/adbc-snowflake/src/statement.rs @@ -15,6 +15,7 @@ use crate::driver::Inner; pub struct Statement { pub(crate) inner: Arc, pub(crate) stmt_handle: Handle, + pub(crate) conn_handle: Handle, pub(crate) query: Option, pub(crate) target_table: Option, pub(crate) ingest_mode: Option, @@ -108,7 +109,19 @@ impl adbc_core::Statement for Statement { Error::with_message_and_status("cannot execute without a query", Status::InvalidState) })?; - // NOTE: query_tag SET not yet implemented — conn_handle added in Task 8 + // Set QUERY_TAG session parameter if configured + if let Some(ref tag) = self.query_tag { + let escaped = tag.replace('\'', "''"); + let set_sql = format!("ALTER SESSION SET QUERY_TAG = '{escaped}'"); + let tmp_handle = self.inner.sf.statement_new(self.conn_handle) + .map_err(crate::error::api_error_to_adbc_error)?; + let set_result = self.inner.runtime.block_on(async { + self.inner.sf.statement_set_sql_query(tmp_handle, set_sql).await?; + self.inner.sf.statement_execute_query(tmp_handle, None).await + }); + let _ = self.inner.sf.statement_release(tmp_handle); + set_result.map_err(crate::error::api_error_to_adbc_error)?; + } let result = self.inner.runtime.block_on(async { self.inner.sf.statement_set_sql_query(self.stmt_handle, query).await?; @@ -133,7 +146,19 @@ impl adbc_core::Statement for Statement { Error::with_message_and_status("cannot execute without a query", Status::InvalidState) })?; - // NOTE: query_tag SET not yet implemented — conn_handle added in Task 8 + // Set QUERY_TAG session parameter if configured + if let Some(ref tag) = self.query_tag { + let escaped = tag.replace('\'', "''"); + let set_sql = format!("ALTER SESSION SET QUERY_TAG = '{escaped}'"); + let tmp_handle = self.inner.sf.statement_new(self.conn_handle) + .map_err(crate::error::api_error_to_adbc_error)?; + let set_result = self.inner.runtime.block_on(async { + self.inner.sf.statement_set_sql_query(tmp_handle, set_sql).await?; + self.inner.sf.statement_execute_query(tmp_handle, None).await + }); + let _ = self.inner.sf.statement_release(tmp_handle); + set_result.map_err(crate::error::api_error_to_adbc_error)?; + } let result = self.inner.runtime.block_on(async { self.inner.sf.statement_set_sql_query(self.stmt_handle, query).await?; @@ -190,6 +215,7 @@ mod tests { Statement { inner: driver.inner.clone(), stmt_handle: sf_core::apis::database_driver_v1::Handle { id: 0, magic: 0 }, + conn_handle: sf_core::apis::database_driver_v1::Handle { id: 0, magic: 0 }, query: None, target_table: None, ingest_mode: None, @@ -219,6 +245,7 @@ mod tests { let mut stmt = Statement { inner: driver.inner.clone(), stmt_handle: sf_core::apis::database_driver_v1::Handle { id: 0, magic: 0 }, + conn_handle: sf_core::apis::database_driver_v1::Handle { id: 0, magic: 0 }, query: None, target_table: Some("mytable".into()), ingest_mode: None, @@ -236,6 +263,7 @@ mod tests { let mut stmt = Statement { inner: driver.inner.clone(), stmt_handle: sf_core::apis::database_driver_v1::Handle { id: 0, magic: 0 }, + conn_handle: sf_core::apis::database_driver_v1::Handle { id: 0, magic: 0 }, query: None, target_table: Some("mytable".into()), ingest_mode: None, @@ -275,4 +303,19 @@ mod tests { ).unwrap_err(); assert_eq!(err.status, adbc_core::error::Status::NotFound); } + + #[test] + fn set_query_tag_stored_and_readable() { + let mut stmt = make_stmt(); + stmt.set_option( + OptionStatement::Other("adbc.snowflake.statement.query_tag".into()), + OptionValue::String("my_tag".into()), + ).unwrap(); + assert_eq!( + stmt.get_option_string(OptionStatement::Other("adbc.snowflake.statement.query_tag".into())).unwrap(), + "my_tag" + ); + // Verify conn_handle is present on the struct (compile-time check) + let _ = stmt.conn_handle; + } } From d0fef93278818fb1e75073754073a1a75519cccc Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Thu, 19 Mar 2026 16:01:12 -0400 Subject: [PATCH 14/76] test(rust): add integration tests for adbc-snowflake --- rust/adbc-snowflake/tests/integration.rs | 123 +++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 rust/adbc-snowflake/tests/integration.rs diff --git a/rust/adbc-snowflake/tests/integration.rs b/rust/adbc-snowflake/tests/integration.rs new file mode 100644 index 0000000..7638d85 --- /dev/null +++ b/rust/adbc-snowflake/tests/integration.rs @@ -0,0 +1,123 @@ +// tests/integration.rs +use adbc_core::{ + options::{OptionDatabase, OptionValue}, + Connection as _, Database as _, Driver as _, Optionable, Statement as _, +}; +use adbc_snowflake::Driver; + +fn get_env(key: &str) -> Option { + std::env::var(key).ok().filter(|s| !s.is_empty()) +} + +fn make_connection() -> Option { + let account = get_env("SNOWFLAKE_TEST_ACCOUNT")?; + let user = get_env("SNOWFLAKE_TEST_USER")?; + let password = get_env("SNOWFLAKE_TEST_PASSWORD")?; + + let mut driver = Driver::default(); + let mut db = driver.new_database().expect("new_database"); + db.set_option( + OptionDatabase::Other("adbc.snowflake.sql.account".into()), + OptionValue::String(account), + ).expect("set account"); + db.set_option(OptionDatabase::Username, OptionValue::String(user)) + .expect("set user"); + db.set_option(OptionDatabase::Password, OptionValue::String(password)) + .expect("set password"); + + if let Some(wh) = get_env("SNOWFLAKE_TEST_WAREHOUSE") { + db.set_option( + OptionDatabase::Other("adbc.snowflake.sql.warehouse".into()), + OptionValue::String(wh), + ).expect("set warehouse"); + } + if let Some(db_name) = get_env("SNOWFLAKE_TEST_DATABASE") { + db.set_option( + OptionDatabase::Other("adbc.snowflake.sql.db".into()), + OptionValue::String(db_name), + ).expect("set database"); + } + if let Some(schema) = get_env("SNOWFLAKE_TEST_SCHEMA") { + db.set_option( + OptionDatabase::Other("adbc.snowflake.sql.schema".into()), + OptionValue::String(schema), + ).expect("set schema"); + } + + Some(db.new_connection().expect("new_connection")) +} + +#[test] +fn test_select_one() { + let Some(mut conn) = make_connection() else { + eprintln!("Skipping: SNOWFLAKE_TEST_ACCOUNT/USER/PASSWORD not set"); + return; + }; + let mut stmt = conn.new_statement().expect("new_statement"); + stmt.set_sql_query("SELECT 1 AS n").expect("set_sql_query"); + let mut reader = stmt.execute().expect("execute"); + let batch = reader.next().expect("no batch").expect("batch error"); + assert_eq!(batch.num_rows(), 1); + assert_eq!(batch.num_columns(), 1); +} + +#[test] +fn test_get_table_types() { + let Some(conn) = make_connection() else { + eprintln!("Skipping: SNOWFLAKE_TEST_ACCOUNT/USER/PASSWORD not set"); + return; + }; + use arrow_array::cast::AsArray; + let mut reader = conn.get_table_types().expect("get_table_types"); + let batch = reader.next().expect("no batch").expect("batch error"); + let types: Vec<&str> = batch.column(0).as_string::().iter() + .filter_map(|v| v).collect(); + assert!(types.contains(&"TABLE")); + assert!(types.contains(&"VIEW")); +} + +#[test] +fn test_get_info_no_codes() { + let Some(conn) = make_connection() else { + eprintln!("Skipping: SNOWFLAKE_TEST_ACCOUNT/USER/PASSWORD not set"); + return; + }; + let mut reader = conn.get_info(None).expect("get_info"); + let batch = reader.next().expect("no batch").expect("batch error"); + assert!(batch.num_rows() > 0, "expected at least one info row"); +} + +#[test] +fn test_execute_ddl_and_dml() { + let Some(mut conn) = make_connection() else { + eprintln!("Skipping: SNOWFLAKE_TEST_ACCOUNT/USER/PASSWORD not set"); + return; + }; + + { + let mut stmt = conn.new_statement().unwrap(); + stmt.set_sql_query("CREATE OR REPLACE TEMP TABLE adbc_rust_test (id INTEGER, name TEXT)").unwrap(); + stmt.execute_update().expect("create table"); + } + + { + let mut stmt = conn.new_statement().unwrap(); + stmt.set_sql_query("INSERT INTO adbc_rust_test VALUES (1, 'hello')").unwrap(); + let rows = stmt.execute_update().expect("insert"); + assert_eq!(rows, Some(1)); + } + + { + let mut stmt = conn.new_statement().unwrap(); + stmt.set_sql_query("SELECT * FROM adbc_rust_test").unwrap(); + let mut reader = stmt.execute().expect("select"); + let batch = reader.next().expect("no batch").expect("batch error"); + assert_eq!(batch.num_rows(), 1); + } + + { + let mut stmt = conn.new_statement().unwrap(); + stmt.set_sql_query("DROP TABLE IF EXISTS adbc_rust_test").unwrap(); + stmt.execute_update().expect("drop table"); + } +} From d7978b5731cb495817e0501799277c436a1cec1e Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Thu, 19 Mar 2026 16:13:58 -0400 Subject: [PATCH 15/76] chore(rust): clippy fixes and cargo fmt for adbc-snowflake Co-Authored-By: Claude Sonnet 4.6 (1M context) --- rust/adbc-snowflake/src/connection.rs | 50 ++++----- rust/adbc-snowflake/src/database.rs | 84 +++++++-------- rust/adbc-snowflake/src/driver.rs | 2 +- rust/adbc-snowflake/src/statement.rs | 130 +++++++++++++++++------ rust/adbc-snowflake/tests/integration.rs | 31 ++++-- 5 files changed, 183 insertions(+), 114 deletions(-) diff --git a/rust/adbc-snowflake/src/connection.rs b/rust/adbc-snowflake/src/connection.rs index 4259ae2..1d55f05 100644 --- a/rust/adbc-snowflake/src/connection.rs +++ b/rust/adbc-snowflake/src/connection.rs @@ -3,10 +3,10 @@ use std::collections::HashSet; use std::sync::Arc; use adbc_core::{ - constants, + Optionable, constants, error::{Error, Result, Status}, options::{InfoCode, ObjectDepth, OptionConnection, OptionValue}, - schemas, Optionable, + schemas, }; use arrow_array::{ ArrayRef, BooleanArray, Int64Array, RecordBatch, RecordBatchReader, StringArray, UInt32Array, @@ -40,7 +40,10 @@ struct SingleBatchReader { impl SingleBatchReader { fn new(batch: RecordBatch) -> Self { let schema = batch.schema(); - Self { batch: Some(batch), schema } + Self { + batch: Some(batch), + schema, + } } } @@ -112,17 +115,14 @@ impl Optionable for Connection { return Err(Error::with_message_and_status( "autocommit value must be a string", Status::InvalidArguments, - )) + )); } }; self.set_autocommit(enabled) } OptionConnection::CurrentCatalog => { if let OptionValue::String(s) = &value { - self.execute_simple(&format!( - r#"USE DATABASE "{}""#, - s.replace('"', "\"\"") - )) + self.execute_simple(&format!(r#"USE DATABASE "{}""#, s.replace('"', "\"\""))) } else { Err(Error::with_message_and_status( "current_catalog value must be a string", @@ -132,10 +132,7 @@ impl Optionable for Connection { } OptionConnection::CurrentSchema => { if let OptionValue::String(s) = &value { - self.execute_simple(&format!( - r#"USE SCHEMA "{}""#, - s.replace('"', "\"\"") - )) + self.execute_simple(&format!(r#"USE SCHEMA "{}""#, s.replace('"', "\"\""))) } else { Err(Error::with_message_and_status( "current_schema value must be a string", @@ -203,6 +200,7 @@ impl adbc_core::Connection for Connection { Err(crate::error::not_implemented("cancel")) } + #[allow(refining_impl_trait)] fn get_info( &self, codes: Option>, @@ -230,10 +228,7 @@ impl adbc_core::Connection for Connection { return Ok(Box::new(SingleBatchReader::new(batch))); } - let name_vals: Vec = selected - .iter() - .map(|(c, _, _)| u32::from(c)) - .collect(); + let name_vals: Vec = selected.iter().map(|(c, _, _)| u32::from(c)).collect(); let type_ids: Vec = selected.iter().map(|(_, t, _)| *t).collect(); let offsets: Vec = selected.iter().map(|(_, _, o)| *o).collect(); @@ -247,8 +242,7 @@ impl adbc_core::Connection for Connection { let bool_values = Arc::new(BooleanArray::from(vec![true, false])) as ArrayRef; let int64_values = Arc::new(Int64Array::from(vec![constants::ADBC_VERSION_1_1_0 as i64])) as ArrayRef; - let int32_values = - Arc::new(arrow_array::Int32Array::from(vec![0i32])) as ArrayRef; + let int32_values = Arc::new(arrow_array::Int32Array::from(vec![0i32])) as ArrayRef; let list_values = Arc::new(arrow_array::ListArray::new_null( Arc::new(Field::new("item", DataType::Utf8, true)), 0, @@ -304,6 +298,7 @@ impl adbc_core::Connection for Connection { Ok(Box::new(SingleBatchReader::new(batch))) } + #[allow(refining_impl_trait)] fn get_objects( &self, _depth: ObjectDepth, @@ -353,13 +348,13 @@ impl adbc_core::Connection for Connection { // Safety: exec_result.stream is a valid FFI stream from sf_core. We take ownership // via Box::into_raw and transfer it to ArrowArrayStreamReader. The C ABI layout is // stable across arrow versions per the Arrow C Data Interface specification. - let raw = Box::into_raw(exec_result.stream) - as *mut arrow_array::ffi_stream::FFI_ArrowArrayStream; - let mut reader = unsafe { arrow_array::ffi_stream::ArrowArrayStreamReader::from_raw(raw) } + let raw = + Box::into_raw(exec_result.stream) as *mut arrow_array::ffi_stream::FFI_ArrowArrayStream; + let reader = unsafe { arrow_array::ffi_stream::ArrowArrayStreamReader::from_raw(raw) } .map_err(|e| Error::with_message_and_status(e.to_string(), Status::IO))?; let mut fields: Vec = Vec::new(); - while let Some(batch) = reader.next() { + for batch in reader { let batch = batch.map_err(|e| Error::with_message_and_status(e.to_string(), Status::IO))?; if batch.num_columns() < 4 { @@ -381,20 +376,20 @@ impl adbc_core::Connection for Connection { Ok(Schema::new(fields)) } + #[allow(refining_impl_trait)] fn get_table_types(&self) -> Result> { let array = Arc::new(StringArray::from(vec!["TABLE", "VIEW"])); - let batch = - RecordBatch::try_new(schemas::GET_TABLE_TYPES_SCHEMA.clone(), vec![array]) - .map_err(|e| { - Error::with_message_and_status(e.to_string(), Status::Internal) - })?; + let batch = RecordBatch::try_new(schemas::GET_TABLE_TYPES_SCHEMA.clone(), vec![array]) + .map_err(|e| Error::with_message_and_status(e.to_string(), Status::Internal))?; Ok(Box::new(SingleBatchReader::new(batch))) } + #[allow(refining_impl_trait)] fn get_statistic_names(&self) -> Result> { Err(crate::error::not_implemented("get_statistic_names")) } + #[allow(refining_impl_trait)] fn get_statistics( &self, _catalog: Option<&str>, @@ -423,6 +418,7 @@ impl adbc_core::Connection for Connection { Ok(()) } + #[allow(refining_impl_trait)] fn read_partition( &self, _partition: impl AsRef<[u8]>, diff --git a/rust/adbc-snowflake/src/database.rs b/rust/adbc-snowflake/src/database.rs index fc8bf41..9d0565f 100644 --- a/rust/adbc-snowflake/src/database.rs +++ b/rust/adbc-snowflake/src/database.rs @@ -3,9 +3,9 @@ use std::collections::HashMap; use std::sync::Arc; use adbc_core::{ + Optionable, error::{Error, Result, Status}, options::{OptionConnection, OptionDatabase, OptionValue}, - Optionable, }; use sf_core::apis::database_driver_v1::Handle; use sf_core::config::settings::Setting; @@ -26,7 +26,7 @@ fn adbc_db_opt_to_sf(key: &str, value: &OptionValue) -> Result Result "authenticator".to_string(), "adbc.snowflake.sql.client_option.auth_token" => "token".to_string(), "adbc.snowflake.sql.client_option.jwt_private_key" => "private_key_file".to_string(), - "adbc.snowflake.sql.client_option.jwt_private_key_pkcs8_value" => { - "private_key".to_string() - } + "adbc.snowflake.sql.client_option.jwt_private_key_pkcs8_value" => "private_key".to_string(), "adbc.snowflake.sql.client_option.jwt_private_key_pkcs8_password" => { "private_key_password".to_string() } @@ -62,7 +60,7 @@ fn adbc_db_opt_to_sf(key: &str, value: &OptionValue) -> Result Result> { let key_str = key.as_ref(); - if let Ok(Some((param, _))) = adbc_db_opt_to_sf(key_str, &OptionValue::Bytes(vec![])) { - if let Some(Setting::Bytes(b)) = self.sf_settings.get(¶m) { - return Ok(b.clone()); - } + if let Ok(Some((param, _))) = adbc_db_opt_to_sf(key_str, &OptionValue::Bytes(vec![])) + && let Some(Setting::Bytes(b)) = self.sf_settings.get(¶m) + { + return Ok(b.clone()); } Err(Error::with_message_and_status( format!("option not found: {key_str}"), @@ -146,10 +143,10 @@ impl Optionable for Database { fn get_option_int(&self, key: Self::Option) -> Result { let key_str = key.as_ref(); - if let Ok(Some((param, _))) = adbc_db_opt_to_sf(key_str, &OptionValue::Int(0)) { - if let Some(Setting::Int(i)) = self.sf_settings.get(¶m) { - return Ok(*i); - } + if let Ok(Some((param, _))) = adbc_db_opt_to_sf(key_str, &OptionValue::Int(0)) + && let Some(Setting::Int(i)) = self.sf_settings.get(¶m) + { + return Ok(*i); } Err(Error::with_message_and_status( format!("option not found: {key_str}"), @@ -159,10 +156,10 @@ impl Optionable for Database { fn get_option_double(&self, key: Self::Option) -> Result { let key_str = key.as_ref(); - if let Ok(Some((param, _))) = adbc_db_opt_to_sf(key_str, &OptionValue::Double(0.0)) { - if let Some(Setting::Double(d)) = self.sf_settings.get(¶m) { - return Ok(*d); - } + if let Ok(Some((param, _))) = adbc_db_opt_to_sf(key_str, &OptionValue::Double(0.0)) + && let Some(Setting::Double(d)) = self.sf_settings.get(¶m) + { + return Ok(*d); } Err(Error::with_message_and_status( format!("option not found: {key_str}"), @@ -180,10 +177,7 @@ impl Database { /// Username/Password directly when credentials contain special characters. /// Query parameter values are not URL-decoded. fn apply_uri(&mut self, uri: String) -> Result<()> { - let stripped = uri - .strip_prefix("snowflake://") - .unwrap_or(&uri) - .to_string(); + let stripped = uri.strip_prefix("snowflake://").unwrap_or(&uri).to_string(); let (user_info, rest) = if let Some(at) = stripped.find('@') { ( @@ -320,7 +314,7 @@ impl adbc_core::Database for Database { return Err(Error::with_message_and_status( "unsupported option value type", Status::InvalidArguments, - )) + )); } }; self.inner @@ -353,16 +347,10 @@ impl adbc_core::Database for Database { conn.set_autocommit(ac)?; } if let Some(cat) = post_catalog { - conn.execute_simple(&format!( - r#"USE DATABASE "{}""#, - cat.replace('"', "\"\"") - ))?; + conn.execute_simple(&format!(r#"USE DATABASE "{}""#, cat.replace('"', "\"\"")))?; } if let Some(sch) = post_schema { - conn.execute_simple(&format!( - r#"USE SCHEMA "{}""#, - sch.replace('"', "\"\"") - ))?; + conn.execute_simple(&format!(r#"USE SCHEMA "{}""#, sch.replace('"', "\"\"")))?; } Ok(conn) @@ -372,7 +360,10 @@ impl adbc_core::Database for Database { #[cfg(test)] mod tests { use super::*; - use adbc_core::{options::{OptionDatabase, OptionValue}, Driver as _}; + use adbc_core::{ + Driver as _, + options::{OptionDatabase, OptionValue}, + }; fn make_db() -> Database { let mut driver = crate::driver::Driver::default(); @@ -388,10 +379,8 @@ mod tests { ) .unwrap(); assert_eq!( - db.get_option_string(OptionDatabase::Other( - "adbc.snowflake.sql.account".into() - )) - .unwrap(), + db.get_option_string(OptionDatabase::Other("adbc.snowflake.sql.account".into())) + .unwrap(), "myaccount" ); } @@ -411,8 +400,11 @@ mod tests { #[test] fn username_maps_to_user_param() { let mut db = make_db(); - db.set_option(OptionDatabase::Username, OptionValue::String("alice".into())) - .unwrap(); + db.set_option( + OptionDatabase::Username, + OptionValue::String("alice".into()), + ) + .unwrap(); let setting = db.sf_settings.get("user").unwrap(); assert_eq!( *setting, diff --git a/rust/adbc-snowflake/src/driver.rs b/rust/adbc-snowflake/src/driver.rs index 279d36d..2873ce0 100644 --- a/rust/adbc-snowflake/src/driver.rs +++ b/rust/adbc-snowflake/src/driver.rs @@ -2,9 +2,9 @@ use std::sync::Arc; use adbc_core::{ + Optionable, error::{Error, Result, Status}, options::{OptionDatabase, OptionValue}, - Optionable, }; use sf_core::apis::database_driver_v1::DatabaseDriverV1; use tokio::runtime::Runtime; diff --git a/rust/adbc-snowflake/src/statement.rs b/rust/adbc-snowflake/src/statement.rs index 0a100f3..a43245e 100644 --- a/rust/adbc-snowflake/src/statement.rs +++ b/rust/adbc-snowflake/src/statement.rs @@ -2,9 +2,9 @@ use std::sync::Arc; use adbc_core::{ + Optionable, PartitionedResult, error::{Error, Result, Status}, options::{OptionStatement, OptionValue}, - Optionable, PartitionedResult, }; use arrow_array::{RecordBatch, RecordBatchReader}; use arrow_schema::Schema; @@ -39,7 +39,10 @@ impl Optionable for Statement { self.target_table = Some(s); Ok(()) } else { - Err(Error::with_message_and_status("target_table must be a string", Status::InvalidArguments)) + Err(Error::with_message_and_status( + "target_table must be a string", + Status::InvalidArguments, + )) } } OptionStatement::IngestMode => { @@ -47,7 +50,10 @@ impl Optionable for Statement { self.ingest_mode = Some(s); Ok(()) } else { - Err(Error::with_message_and_status("ingest_mode must be a string", Status::InvalidArguments)) + Err(Error::with_message_and_status( + "ingest_mode must be a string", + Status::InvalidArguments, + )) } } OptionStatement::Other(ref k) if k == "adbc.snowflake.statement.query_tag" => { @@ -55,7 +61,10 @@ impl Optionable for Statement { self.query_tag = Some(s); Ok(()) } else { - Err(Error::with_message_and_status("query_tag must be a string", Status::InvalidArguments)) + Err(Error::with_message_and_status( + "query_tag must be a string", + Status::InvalidArguments, + )) } } _ => Err(Error::with_message_and_status( @@ -78,15 +87,24 @@ impl Optionable for Statement { } fn get_option_bytes(&self, _key: Self::Option) -> Result> { - Err(Error::with_message_and_status("option not found", Status::NotFound)) + Err(Error::with_message_and_status( + "option not found", + Status::NotFound, + )) } fn get_option_int(&self, _key: Self::Option) -> Result { - Err(Error::with_message_and_status("option not found", Status::NotFound)) + Err(Error::with_message_and_status( + "option not found", + Status::NotFound, + )) } fn get_option_double(&self, _key: Self::Option) -> Result { - Err(Error::with_message_and_status("option not found", Status::NotFound)) + Err(Error::with_message_and_status( + "option not found", + Status::NotFound, + )) } } @@ -99,6 +117,7 @@ impl adbc_core::Statement for Statement { Err(crate::error::not_implemented("bind_stream")) } + #[allow(refining_impl_trait)] fn execute(&mut self) -> Result> { if self.target_table.is_some() { return Err(crate::error::not_implemented( @@ -113,24 +132,44 @@ impl adbc_core::Statement for Statement { if let Some(ref tag) = self.query_tag { let escaped = tag.replace('\'', "''"); let set_sql = format!("ALTER SESSION SET QUERY_TAG = '{escaped}'"); - let tmp_handle = self.inner.sf.statement_new(self.conn_handle) + let tmp_handle = self + .inner + .sf + .statement_new(self.conn_handle) .map_err(crate::error::api_error_to_adbc_error)?; let set_result = self.inner.runtime.block_on(async { - self.inner.sf.statement_set_sql_query(tmp_handle, set_sql).await?; - self.inner.sf.statement_execute_query(tmp_handle, None).await + self.inner + .sf + .statement_set_sql_query(tmp_handle, set_sql) + .await?; + self.inner + .sf + .statement_execute_query(tmp_handle, None) + .await }); let _ = self.inner.sf.statement_release(tmp_handle); set_result.map_err(crate::error::api_error_to_adbc_error)?; } - let result = self.inner.runtime.block_on(async { - self.inner.sf.statement_set_sql_query(self.stmt_handle, query).await?; - self.inner.sf.statement_execute_query(self.stmt_handle, None).await - }).map_err(crate::error::api_error_to_adbc_error)?; + let result = self + .inner + .runtime + .block_on(async { + self.inner + .sf + .statement_set_sql_query(self.stmt_handle, query) + .await?; + self.inner + .sf + .statement_execute_query(self.stmt_handle, None) + .await + }) + .map_err(crate::error::api_error_to_adbc_error)?; // Safety: result.stream is a valid FFI stream from sf_core. Ownership is transferred // to ArrowArrayStreamReader. The C ABI layout is stable per the Arrow C Data Interface. - let raw = Box::into_raw(result.stream) as *mut arrow_array::ffi_stream::FFI_ArrowArrayStream; + let raw = + Box::into_raw(result.stream) as *mut arrow_array::ffi_stream::FFI_ArrowArrayStream; let reader = unsafe { arrow_array::ffi_stream::ArrowArrayStreamReader::from_raw(raw) } .map_err(|e| Error::with_message_and_status(e.to_string(), Status::IO))?; Ok(Box::new(reader)) @@ -150,20 +189,39 @@ impl adbc_core::Statement for Statement { if let Some(ref tag) = self.query_tag { let escaped = tag.replace('\'', "''"); let set_sql = format!("ALTER SESSION SET QUERY_TAG = '{escaped}'"); - let tmp_handle = self.inner.sf.statement_new(self.conn_handle) + let tmp_handle = self + .inner + .sf + .statement_new(self.conn_handle) .map_err(crate::error::api_error_to_adbc_error)?; let set_result = self.inner.runtime.block_on(async { - self.inner.sf.statement_set_sql_query(tmp_handle, set_sql).await?; - self.inner.sf.statement_execute_query(tmp_handle, None).await + self.inner + .sf + .statement_set_sql_query(tmp_handle, set_sql) + .await?; + self.inner + .sf + .statement_execute_query(tmp_handle, None) + .await }); let _ = self.inner.sf.statement_release(tmp_handle); set_result.map_err(crate::error::api_error_to_adbc_error)?; } - let result = self.inner.runtime.block_on(async { - self.inner.sf.statement_set_sql_query(self.stmt_handle, query).await?; - self.inner.sf.statement_execute_query(self.stmt_handle, None).await - }).map_err(crate::error::api_error_to_adbc_error)?; + let result = self + .inner + .runtime + .block_on(async { + self.inner + .sf + .statement_set_sql_query(self.stmt_handle, query) + .await?; + self.inner + .sf + .statement_execute_query(self.stmt_handle, None) + .await + }) + .map_err(crate::error::api_error_to_adbc_error)?; Ok(result.rows_affected) } @@ -197,7 +255,9 @@ impl adbc_core::Statement for Statement { } fn set_substrait_plan(&mut self, _plan: impl AsRef<[u8]>) -> Result<()> { - Err(crate::error::not_implemented("Snowflake does not support Substrait plans")) + Err(crate::error::not_implemented( + "Snowflake does not support Substrait plans", + )) } fn cancel(&mut self) -> Result<()> { @@ -290,17 +350,23 @@ mod tests { #[test] fn set_target_table_option() { let mut stmt = make_stmt(); - stmt.set_option(OptionStatement::TargetTable, OptionValue::String("mytable".into())).unwrap(); + stmt.set_option( + OptionStatement::TargetTable, + OptionValue::String("mytable".into()), + ) + .unwrap(); assert_eq!(stmt.target_table.as_deref(), Some("mytable")); } #[test] fn unknown_option_returns_not_found() { let mut stmt = make_stmt(); - let err = stmt.set_option( - OptionStatement::Other("unknown.option".into()), - OptionValue::String("val".into()), - ).unwrap_err(); + let err = stmt + .set_option( + OptionStatement::Other("unknown.option".into()), + OptionValue::String("val".into()), + ) + .unwrap_err(); assert_eq!(err.status, adbc_core::error::Status::NotFound); } @@ -310,9 +376,13 @@ mod tests { stmt.set_option( OptionStatement::Other("adbc.snowflake.statement.query_tag".into()), OptionValue::String("my_tag".into()), - ).unwrap(); + ) + .unwrap(); assert_eq!( - stmt.get_option_string(OptionStatement::Other("adbc.snowflake.statement.query_tag".into())).unwrap(), + stmt.get_option_string(OptionStatement::Other( + "adbc.snowflake.statement.query_tag".into() + )) + .unwrap(), "my_tag" ); // Verify conn_handle is present on the struct (compile-time check) diff --git a/rust/adbc-snowflake/tests/integration.rs b/rust/adbc-snowflake/tests/integration.rs index 7638d85..3469717 100644 --- a/rust/adbc-snowflake/tests/integration.rs +++ b/rust/adbc-snowflake/tests/integration.rs @@ -1,7 +1,7 @@ // tests/integration.rs use adbc_core::{ - options::{OptionDatabase, OptionValue}, Connection as _, Database as _, Driver as _, Optionable, Statement as _, + options::{OptionDatabase, OptionValue}, }; use adbc_snowflake::Driver; @@ -19,7 +19,8 @@ fn make_connection() -> Option { db.set_option( OptionDatabase::Other("adbc.snowflake.sql.account".into()), OptionValue::String(account), - ).expect("set account"); + ) + .expect("set account"); db.set_option(OptionDatabase::Username, OptionValue::String(user)) .expect("set user"); db.set_option(OptionDatabase::Password, OptionValue::String(password)) @@ -29,19 +30,22 @@ fn make_connection() -> Option { db.set_option( OptionDatabase::Other("adbc.snowflake.sql.warehouse".into()), OptionValue::String(wh), - ).expect("set warehouse"); + ) + .expect("set warehouse"); } if let Some(db_name) = get_env("SNOWFLAKE_TEST_DATABASE") { db.set_option( OptionDatabase::Other("adbc.snowflake.sql.db".into()), OptionValue::String(db_name), - ).expect("set database"); + ) + .expect("set database"); } if let Some(schema) = get_env("SNOWFLAKE_TEST_SCHEMA") { db.set_option( OptionDatabase::Other("adbc.snowflake.sql.schema".into()), OptionValue::String(schema), - ).expect("set schema"); + ) + .expect("set schema"); } Some(db.new_connection().expect("new_connection")) @@ -70,8 +74,12 @@ fn test_get_table_types() { use arrow_array::cast::AsArray; let mut reader = conn.get_table_types().expect("get_table_types"); let batch = reader.next().expect("no batch").expect("batch error"); - let types: Vec<&str> = batch.column(0).as_string::().iter() - .filter_map(|v| v).collect(); + let types: Vec<&str> = batch + .column(0) + .as_string::() + .iter() + .filter_map(|v| v) + .collect(); assert!(types.contains(&"TABLE")); assert!(types.contains(&"VIEW")); } @@ -96,13 +104,15 @@ fn test_execute_ddl_and_dml() { { let mut stmt = conn.new_statement().unwrap(); - stmt.set_sql_query("CREATE OR REPLACE TEMP TABLE adbc_rust_test (id INTEGER, name TEXT)").unwrap(); + stmt.set_sql_query("CREATE OR REPLACE TEMP TABLE adbc_rust_test (id INTEGER, name TEXT)") + .unwrap(); stmt.execute_update().expect("create table"); } { let mut stmt = conn.new_statement().unwrap(); - stmt.set_sql_query("INSERT INTO adbc_rust_test VALUES (1, 'hello')").unwrap(); + stmt.set_sql_query("INSERT INTO adbc_rust_test VALUES (1, 'hello')") + .unwrap(); let rows = stmt.execute_update().expect("insert"); assert_eq!(rows, Some(1)); } @@ -117,7 +127,8 @@ fn test_execute_ddl_and_dml() { { let mut stmt = conn.new_statement().unwrap(); - stmt.set_sql_query("DROP TABLE IF EXISTS adbc_rust_test").unwrap(); + stmt.set_sql_query("DROP TABLE IF EXISTS adbc_rust_test") + .unwrap(); stmt.execute_update().expect("drop table"); } } From 410f736560e4d3eaa58155e1e1a7184036aaa459 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Thu, 19 Mar 2026 16:22:54 -0400 Subject: [PATCH 16/76] fix(rust): replace filter_map identity with flatten per clippy --- rust/adbc-snowflake/src/connection.rs | 2 +- rust/adbc-snowflake/tests/integration.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rust/adbc-snowflake/src/connection.rs b/rust/adbc-snowflake/src/connection.rs index 1d55f05..f7af39c 100644 --- a/rust/adbc-snowflake/src/connection.rs +++ b/rust/adbc-snowflake/src/connection.rs @@ -532,7 +532,7 @@ mod tests { .column(0) .as_string::() .iter() - .filter_map(|v| v) + .flatten() .collect(); assert_eq!(types, vec!["TABLE", "VIEW"]); } diff --git a/rust/adbc-snowflake/tests/integration.rs b/rust/adbc-snowflake/tests/integration.rs index 3469717..7c4f168 100644 --- a/rust/adbc-snowflake/tests/integration.rs +++ b/rust/adbc-snowflake/tests/integration.rs @@ -78,7 +78,7 @@ fn test_get_table_types() { .column(0) .as_string::() .iter() - .filter_map(|v| v) + .flatten() .collect(); assert!(types.contains(&"TABLE")); assert!(types.contains(&"VIEW")); From 96a59ef3f85f457e404b2003ebe75fd23139b69e Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Thu, 19 Mar 2026 16:32:12 -0400 Subject: [PATCH 17/76] fix(rust): correct get_info map_values type to match GET_INFO_SCHEMA Replace the incorrect List arm-5 placeholder in get_info() with a proper empty MapArray (Map>) so that RecordBatch::try_new schema validation succeeds. Also fix the UnionFields arm-5 declaration from Field::new_list to Field::new_map to match the adbc_core GET_INFO_SCHEMA. --- rust/adbc-snowflake/src/connection.rs | 48 +++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/rust/adbc-snowflake/src/connection.rs b/rust/adbc-snowflake/src/connection.rs index f7af39c..0baf11f 100644 --- a/rust/adbc-snowflake/src/connection.rs +++ b/rust/adbc-snowflake/src/connection.rs @@ -247,10 +247,43 @@ impl adbc_core::Connection for Connection { Arc::new(Field::new("item", DataType::Utf8, true)), 0, )) as ArrayRef; - let map_values = Arc::new(arrow_array::ListArray::new_null( - Arc::new(Field::new("item", DataType::Utf8, true)), - 0, - )) as ArrayRef; + // arm 5: int32_to_int32_list_map — proper empty MapArray to satisfy schema type check + // (This arm is never selected, but must have the right type for RecordBatch::try_new) + let empty_int32_list_inner = arrow_array::Int32Array::from(Vec::::new()); + let empty_int32_list = arrow_array::ListArray::new( + Arc::new(Field::new_list_field(DataType::Int32, true)), + arrow_buffer::OffsetBuffer::new(arrow_buffer::ScalarBuffer::from(vec![0i32])), + Arc::new(empty_int32_list_inner), + None, + ); + let empty_entries = arrow_array::StructArray::new( + arrow_schema::Fields::from(vec![ + Field::new("key", DataType::Int32, false), + Field::new_list("value", Field::new_list_field(DataType::Int32, true), true), + ]), + vec![ + Arc::new(arrow_array::Int32Array::from(Vec::::new())) as ArrayRef, + Arc::new(empty_int32_list) as ArrayRef, + ], + None, + ); + let map_values = Arc::new( + arrow_array::MapArray::try_new( + Arc::new(Field::new_struct( + "entries", + vec![ + Field::new("key", DataType::Int32, false), + Field::new_list("value", Field::new_list_field(DataType::Int32, true), true), + ], + false, + )), + arrow_buffer::OffsetBuffer::new(arrow_buffer::ScalarBuffer::from(vec![0i32])), + empty_entries, + None, + false, + ) + .map_err(|e| Error::with_message_and_status(e.to_string(), Status::Internal))?, + ) as ArrayRef; let union_array = UnionArray::try_new( #[allow(deprecated)] @@ -266,9 +299,12 @@ impl adbc_core::Connection for Connection { Field::new_list_field(DataType::Utf8, true), true, ), - Field::new_list( + Field::new_map( "int32_to_int32_list_map", - Field::new_list_field(DataType::Int32, true), + "entries", + Field::new("key", DataType::Int32, false), + Field::new_list("value", Field::new_list_field(DataType::Int32, true), true), + false, true, ), ], From e98066f125673b775b78d1a913177a5cec63ee9c Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Thu, 19 Mar 2026 16:34:02 -0400 Subject: [PATCH 18/76] =?UTF-8?q?fix(rust):=20cleanup=20from=20final=20cod?= =?UTF-8?q?e=20review=20=E2=80=94=20state=20tracking,=20dedup,=20pin=20sf?= =?UTF-8?q?=5Fcore=20rev?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rust/adbc-snowflake/Cargo.toml | 2 +- rust/adbc-snowflake/src/connection.rs | 2 + rust/adbc-snowflake/src/statement.rs | 71 +++++++++++---------------- 3 files changed, 31 insertions(+), 44 deletions(-) diff --git a/rust/adbc-snowflake/Cargo.toml b/rust/adbc-snowflake/Cargo.toml index 5f26683..333c6eb 100644 --- a/rust/adbc-snowflake/Cargo.toml +++ b/rust/adbc-snowflake/Cargo.toml @@ -9,7 +9,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] adbc_core = "0.22.0" adbc_ffi = "0.22.0" -sf_core = { git = "https://github.com/snowflakedb/universal-driver", subdirectory = "sf_core" } +sf_core = { git = "https://github.com/snowflakedb/universal-driver", subdirectory = "sf_core", rev = "080422e05fbd727f68d7c494e564ec625e1375d6" } arrow-array = { version = ">=53.1.0, <59", default-features = false, features = ["ffi"] } arrow-buffer = { version = ">=53.1.0, <59", default-features = false } arrow-schema = { version = ">=53.1.0, <59", default-features = false } diff --git a/rust/adbc-snowflake/src/connection.rs b/rust/adbc-snowflake/src/connection.rs index 0baf11f..7a8667e 100644 --- a/rust/adbc-snowflake/src/connection.rs +++ b/rust/adbc-snowflake/src/connection.rs @@ -438,6 +438,7 @@ impl adbc_core::Connection for Connection { fn commit(&mut self) -> Result<()> { self.execute_simple("COMMIT")?; + self.active_transaction = false; if !self.autocommit { self.execute_simple("BEGIN")?; self.active_transaction = true; @@ -447,6 +448,7 @@ impl adbc_core::Connection for Connection { fn rollback(&mut self) -> Result<()> { self.execute_simple("ROLLBACK")?; + self.active_transaction = false; if !self.autocommit { self.execute_simple("BEGIN")?; self.active_transaction = true; diff --git a/rust/adbc-snowflake/src/statement.rs b/rust/adbc-snowflake/src/statement.rs index a43245e..56fe1e7 100644 --- a/rust/adbc-snowflake/src/statement.rs +++ b/rust/adbc-snowflake/src/statement.rs @@ -108,27 +108,8 @@ impl Optionable for Statement { } } -impl adbc_core::Statement for Statement { - fn bind(&mut self, _batch: RecordBatch) -> Result<()> { - Err(crate::error::not_implemented("bind")) - } - - fn bind_stream(&mut self, _reader: Box) -> Result<()> { - Err(crate::error::not_implemented("bind_stream")) - } - - #[allow(refining_impl_trait)] - fn execute(&mut self) -> Result> { - if self.target_table.is_some() { - return Err(crate::error::not_implemented( - "bulk ingestion (target_table) is not yet implemented", - )); - } - let query = self.query.clone().ok_or_else(|| { - Error::with_message_and_status("cannot execute without a query", Status::InvalidState) - })?; - - // Set QUERY_TAG session parameter if configured +impl Statement { + fn apply_query_tag(&self) -> Result<()> { if let Some(ref tag) = self.query_tag { let escaped = tag.replace('\'', "''"); let set_sql = format!("ALTER SESSION SET QUERY_TAG = '{escaped}'"); @@ -150,6 +131,31 @@ impl adbc_core::Statement for Statement { let _ = self.inner.sf.statement_release(tmp_handle); set_result.map_err(crate::error::api_error_to_adbc_error)?; } + Ok(()) + } +} + +impl adbc_core::Statement for Statement { + fn bind(&mut self, _batch: RecordBatch) -> Result<()> { + Err(crate::error::not_implemented("bind")) + } + + fn bind_stream(&mut self, _reader: Box) -> Result<()> { + Err(crate::error::not_implemented("bind_stream")) + } + + #[allow(refining_impl_trait)] + fn execute(&mut self) -> Result> { + if self.target_table.is_some() { + return Err(crate::error::not_implemented( + "bulk ingestion (target_table) is not yet implemented", + )); + } + let query = self.query.clone().ok_or_else(|| { + Error::with_message_and_status("cannot execute without a query", Status::InvalidState) + })?; + + self.apply_query_tag()?; let result = self .inner @@ -185,28 +191,7 @@ impl adbc_core::Statement for Statement { Error::with_message_and_status("cannot execute without a query", Status::InvalidState) })?; - // Set QUERY_TAG session parameter if configured - if let Some(ref tag) = self.query_tag { - let escaped = tag.replace('\'', "''"); - let set_sql = format!("ALTER SESSION SET QUERY_TAG = '{escaped}'"); - let tmp_handle = self - .inner - .sf - .statement_new(self.conn_handle) - .map_err(crate::error::api_error_to_adbc_error)?; - let set_result = self.inner.runtime.block_on(async { - self.inner - .sf - .statement_set_sql_query(tmp_handle, set_sql) - .await?; - self.inner - .sf - .statement_execute_query(tmp_handle, None) - .await - }); - let _ = self.inner.sf.statement_release(tmp_handle); - set_result.map_err(crate::error::api_error_to_adbc_error)?; - } + self.apply_query_tag()?; let result = self .inner From 7cdc20bc94030e680b74e477946482a416cb2137 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Thu, 19 Mar 2026 16:51:05 -0400 Subject: [PATCH 19/76] use constants for params --- rust/adbc-snowflake/Cargo.lock | 11 ++++---- rust/adbc-snowflake/src/database.rs | 44 ++++++++++++++++------------- 2 files changed, 30 insertions(+), 25 deletions(-) diff --git a/rust/adbc-snowflake/Cargo.lock b/rust/adbc-snowflake/Cargo.lock index e1fe3cb..5ee183a 100644 --- a/rust/adbc-snowflake/Cargo.lock +++ b/rust/adbc-snowflake/Cargo.lock @@ -9,6 +9,7 @@ dependencies = [ "adbc_core", "adbc_ffi", "arrow-array 57.3.0", + "arrow-buffer 57.3.0", "arrow-schema 57.3.0", "sf_core", "tokio", @@ -1479,7 +1480,7 @@ dependencies = [ [[package]] name = "error_trace" version = "0.1.0" -source = "git+https://github.com/snowflakedb/universal-driver#080422e05fbd727f68d7c494e564ec625e1375d6" +source = "git+https://github.com/snowflakedb/universal-driver?rev=080422e05fbd727f68d7c494e564ec625e1375d6#080422e05fbd727f68d7c494e564ec625e1375d6" dependencies = [ "error_trace_derive", "snafu 0.8.9", @@ -1488,7 +1489,7 @@ dependencies = [ [[package]] name = "error_trace_derive" version = "0.1.0" -source = "git+https://github.com/snowflakedb/universal-driver#080422e05fbd727f68d7c494e564ec625e1375d6" +source = "git+https://github.com/snowflakedb/universal-driver?rev=080422e05fbd727f68d7c494e564ec625e1375d6#080422e05fbd727f68d7c494e564ec625e1375d6" dependencies = [ "quote", "syn 2.0.117", @@ -3066,7 +3067,7 @@ dependencies = [ [[package]] name = "proto_generator" version = "0.1.0" -source = "git+https://github.com/snowflakedb/universal-driver#080422e05fbd727f68d7c494e564ec625e1375d6" +source = "git+https://github.com/snowflakedb/universal-driver?rev=080422e05fbd727f68d7c494e564ec625e1375d6#080422e05fbd727f68d7c494e564ec625e1375d6" dependencies = [ "clap", "env_logger", @@ -3083,7 +3084,7 @@ dependencies = [ [[package]] name = "proto_utils" version = "0.1.0" -source = "git+https://github.com/snowflakedb/universal-driver#080422e05fbd727f68d7c494e564ec625e1375d6" +source = "git+https://github.com/snowflakedb/universal-driver?rev=080422e05fbd727f68d7c494e564ec625e1375d6#080422e05fbd727f68d7c494e564ec625e1375d6" [[package]] name = "quinn" @@ -3623,7 +3624,7 @@ dependencies = [ [[package]] name = "sf_core" version = "0.0.0" -source = "git+https://github.com/snowflakedb/universal-driver#080422e05fbd727f68d7c494e564ec625e1375d6" +source = "git+https://github.com/snowflakedb/universal-driver?rev=080422e05fbd727f68d7c494e564ec625e1375d6#080422e05fbd727f68d7c494e564ec625e1375d6" dependencies = [ "arrow", "arrow-ipc", diff --git a/rust/adbc-snowflake/src/database.rs b/rust/adbc-snowflake/src/database.rs index 9d0565f..0617b9a 100644 --- a/rust/adbc-snowflake/src/database.rs +++ b/rust/adbc-snowflake/src/database.rs @@ -8,6 +8,7 @@ use adbc_core::{ options::{OptionConnection, OptionDatabase, OptionValue}, }; use sf_core::apis::database_driver_v1::Handle; +use sf_core::config::param_registry::param_names; use sf_core::config::settings::Setting; use crate::connection::Connection; @@ -31,21 +32,23 @@ fn adbc_db_opt_to_sf(key: &str, value: &OptionValue) -> Result "user".to_string(), - "password" => "password".to_string(), - "adbc.snowflake.sql.account" => "account".to_string(), - "adbc.snowflake.sql.db" => "database".to_string(), - "adbc.snowflake.sql.schema" => "schema".to_string(), - "adbc.snowflake.sql.warehouse" => "warehouse".to_string(), - "adbc.snowflake.sql.role" => "role".to_string(), - "adbc.snowflake.sql.uri.host" => "host".to_string(), - "adbc.snowflake.sql.uri.protocol" => "protocol".to_string(), - "adbc.snowflake.sql.auth_type" => "authenticator".to_string(), - "adbc.snowflake.sql.client_option.auth_token" => "token".to_string(), - "adbc.snowflake.sql.client_option.jwt_private_key" => "private_key_file".to_string(), - "adbc.snowflake.sql.client_option.jwt_private_key_pkcs8_value" => "private_key".to_string(), + "username" => param_names::USER.into(), + "password" => param_names::PASSWORD.into(), + "adbc.snowflake.sql.account" => param_names::ACCOUNT.into(), + "adbc.snowflake.sql.db" => param_names::DATABASE.into(), + "adbc.snowflake.sql.schema" => param_names::SCHEMA.into(), + "adbc.snowflake.sql.warehouse" => param_names::WAREHOUSE.into(), + "adbc.snowflake.sql.role" => param_names::ROLE.into(), + "adbc.snowflake.sql.uri.host" => param_names::HOST.into(), + "adbc.snowflake.sql.uri.protocol" => param_names::PROTOCOL.into(), + "adbc.snowflake.sql.auth_type" => param_names::AUTHENTICATOR.into(), + "adbc.snowflake.sql.client_option.auth_token" => param_names::TOKEN.into(), + "adbc.snowflake.sql.client_option.jwt_private_key" => param_names::PRIVATE_KEY_FILE.into(), + "adbc.snowflake.sql.client_option.jwt_private_key_pkcs8_value" => { + param_names::PRIVATE_KEY.into() + } "adbc.snowflake.sql.client_option.jwt_private_key_pkcs8_password" => { - "private_key_password".to_string() + param_names::PRIVATE_KEY_PASSWORD.into() } "adbc.snowflake.sql.uri.port" => { let port = match value { @@ -63,7 +66,7 @@ fn adbc_db_opt_to_sf(key: &str, value: &OptionValue) -> Result return Ok(None), other => other.to_string(), @@ -364,6 +367,7 @@ mod tests { Driver as _, options::{OptionDatabase, OptionValue}, }; + use sf_core::config::param_registry::param_names; fn make_db() -> Database { let mut driver = crate::driver::Driver::default(); @@ -393,7 +397,7 @@ mod tests { OptionValue::String("443".into()), ) .unwrap(); - let setting = db.sf_settings.get("port").unwrap(); + let setting = db.sf_settings.get(param_names::PORT.as_str()).unwrap(); assert_eq!(*setting, sf_core::config::settings::Setting::Int(443)); } @@ -405,7 +409,7 @@ mod tests { OptionValue::String("alice".into()), ) .unwrap(); - let setting = db.sf_settings.get("user").unwrap(); + let setting = db.sf_settings.get(param_names::USER.as_str()).unwrap(); assert_eq!( *setting, sf_core::config::settings::Setting::String("alice".into()) @@ -421,15 +425,15 @@ mod tests { ) .unwrap(); assert_eq!( - db.sf_settings.get("account").unwrap(), + db.sf_settings.get(param_names::ACCOUNT.as_str()).unwrap(), &sf_core::config::settings::Setting::String("myaccount".into()) ); assert_eq!( - db.sf_settings.get("user").unwrap(), + db.sf_settings.get(param_names::USER.as_str()).unwrap(), &sf_core::config::settings::Setting::String("alice".into()) ); assert_eq!( - db.sf_settings.get("database").unwrap(), + db.sf_settings.get(param_names::DATABASE.as_str()).unwrap(), &sf_core::config::settings::Setting::String("mydb".into()) ); } From 2063893e750cc13a40cee00e321cbfa5da16bf7d Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Fri, 20 Mar 2026 11:26:21 -0400 Subject: [PATCH 20/76] add some integration tests using env vars --- rust/adbc-snowflake/Cargo.toml | 22 ++++++ rust/adbc-snowflake/src/database.rs | 20 +++++ rust/adbc-snowflake/src/driver.rs | 10 ++- rust/adbc-snowflake/tests/integration.rs | 99 ++++++++++++++++++++++-- 4 files changed, 143 insertions(+), 8 deletions(-) diff --git a/rust/adbc-snowflake/Cargo.toml b/rust/adbc-snowflake/Cargo.toml index 333c6eb..1d9e6d2 100644 --- a/rust/adbc-snowflake/Cargo.toml +++ b/rust/adbc-snowflake/Cargo.toml @@ -1,3 +1,25 @@ +# Copyright (c) 2025 ADBC Drivers Contributors +# +# This file has been modified from its original version, which is +# under the Apache License: +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + [package] name = "adbc-snowflake" version = "0.1.0" diff --git a/rust/adbc-snowflake/src/database.rs b/rust/adbc-snowflake/src/database.rs index 0617b9a..0e5857e 100644 --- a/rust/adbc-snowflake/src/database.rs +++ b/rust/adbc-snowflake/src/database.rs @@ -42,6 +42,7 @@ fn adbc_db_opt_to_sf(key: &str, value: &OptionValue) -> Result param_names::HOST.into(), "adbc.snowflake.sql.uri.protocol" => param_names::PROTOCOL.into(), "adbc.snowflake.sql.auth_type" => param_names::AUTHENTICATOR.into(), + "adbc.snowflake.sql.client_option.application" => "client_app_id".to_string(), "adbc.snowflake.sql.client_option.auth_token" => param_names::TOKEN.into(), "adbc.snowflake.sql.client_option.jwt_private_key" => param_names::PRIVATE_KEY_FILE.into(), "adbc.snowflake.sql.client_option.jwt_private_key_pkcs8_value" => { @@ -333,6 +334,25 @@ impl adbc_core::Database for Database { } } + // If neither host nor server_url was provided, derive host from account. + if !self.sf_settings.contains_key(param_names::HOST.as_str()) + && !self.sf_settings.contains_key(param_names::SERVER_URL.as_str()) + { + if let Some(Setting::String(account)) = + self.sf_settings.get(param_names::ACCOUNT.as_str()) + { + let host = format!("{}.snowflakecomputing.com", account); + self.inner + .runtime + .block_on(self.inner.sf.connection_set_option( + conn_handle, + param_names::HOST.into(), + Setting::String(host), + )) + .map_err(crate::error::api_error_to_adbc_error)?; + } + } + // Authenticate self.inner .runtime diff --git a/rust/adbc-snowflake/src/driver.rs b/rust/adbc-snowflake/src/driver.rs index 2873ce0..54229eb 100644 --- a/rust/adbc-snowflake/src/driver.rs +++ b/rust/adbc-snowflake/src/driver.rs @@ -56,10 +56,18 @@ impl adbc_core::Driver for Driver { opts: impl IntoIterator, ) -> Result { let db_handle = self.inner.sf.database_new(); + let mut sf_settings: std::collections::HashMap = + Default::default(); + sf_settings.insert( + "client_app_id".to_string(), + sf_core::config::settings::Setting::String( + concat!("[ADBC][Rust] Snowflake Driver/", env!("CARGO_PKG_VERSION")).to_string(), + ), + ); let mut db = Database { inner: self.inner.clone(), db_handle, - sf_settings: Default::default(), + sf_settings, }; for (key, value) in opts { db.set_option(key, value)?; diff --git a/rust/adbc-snowflake/tests/integration.rs b/rust/adbc-snowflake/tests/integration.rs index 7c4f168..7095990 100644 --- a/rust/adbc-snowflake/tests/integration.rs +++ b/rust/adbc-snowflake/tests/integration.rs @@ -4,6 +4,7 @@ use adbc_core::{ options::{OptionDatabase, OptionValue}, }; use adbc_snowflake::Driver; +use arrow_array::cast::AsArray; fn get_env(key: &str) -> Option { std::env::var(key).ok().filter(|s| !s.is_empty()) @@ -11,8 +12,7 @@ fn get_env(key: &str) -> Option { fn make_connection() -> Option { let account = get_env("SNOWFLAKE_TEST_ACCOUNT")?; - let user = get_env("SNOWFLAKE_TEST_USER")?; - let password = get_env("SNOWFLAKE_TEST_PASSWORD")?; + let user = get_env("SNOWFLAKE_TEST_USER")?; let mut driver = Driver::default(); let mut db = driver.new_database().expect("new_database"); @@ -22,9 +22,7 @@ fn make_connection() -> Option { ) .expect("set account"); db.set_option(OptionDatabase::Username, OptionValue::String(user)) - .expect("set user"); - db.set_option(OptionDatabase::Password, OptionValue::String(password)) - .expect("set password"); + .expect("set user"); if let Some(wh) = get_env("SNOWFLAKE_TEST_WAREHOUSE") { db.set_option( @@ -51,9 +49,97 @@ fn make_connection() -> Option { Some(db.new_connection().expect("new_connection")) } +fn make_private_key_connection() -> Option { + let account = get_env("SNOWFLAKE_TEST_ACCOUNT")?; + let user = get_env("SNOWFLAKE_TEST_USER")?; + let private_key_file = get_env("SNOWFLAKE_TEST_PRIVATE_KEY_FILE")?; + + let mut driver = Driver::default(); + let mut db = driver.new_database().expect("new_database"); + db.set_option( + OptionDatabase::Other("adbc.snowflake.sql.account".into()), + OptionValue::String(account), + ) + .expect("set account"); + db.set_option(OptionDatabase::Username, OptionValue::String(user)) + .expect("set user"); + db.set_option( + OptionDatabase::Other("adbc.snowflake.sql.auth_type".into()), + OptionValue::String("SNOWFLAKE_JWT".into()), + ) + .expect("set auth_type"); + db.set_option( + OptionDatabase::Other("adbc.snowflake.sql.client_option.jwt_private_key".into()), + OptionValue::String(private_key_file), + ) + .expect("set private_key_file"); + if let Some(wh) = get_env("SNOWFLAKE_TEST_WAREHOUSE") { + db.set_option( + OptionDatabase::Other("adbc.snowflake.sql.warehouse".into()), + OptionValue::String(wh), + ) + .expect("set warehouse"); + } + if let Some(role) = get_env("SNOWFLAKE_TEST_ROLE") { + db.set_option( + OptionDatabase::Other("adbc.snowflake.sql.role".into()), + OptionValue::String(role), + ) + .expect("set role"); + } + + Some(db.new_connection().expect("new_connection")) +} + +#[test] +fn test_private_key_simple_query() { + let Some(mut conn) = make_private_key_connection() else { + eprintln!("Skipping: SNOWFLAKE_TEST_ACCOUNT/USER/PRIVATE_KEY_FILE not set"); + return; + }; + + let expected_user = get_env("SNOWFLAKE_TEST_USER").unwrap(); + let expected_warehouse = get_env("SNOWFLAKE_TEST_WAREHOUSE"); + let expected_role = get_env("SNOWFLAKE_TEST_ROLE"); + + let mut stmt = conn.new_statement().expect("new_statement"); + stmt.set_sql_query("SELECT CURRENT_USER(), CURRENT_WAREHOUSE(), CURRENT_ROLE()") + .expect("set_sql_query"); + let mut reader = stmt.execute().expect("execute"); + let batch = reader.next().expect("no batch").expect("batch error"); + + assert_eq!(batch.num_rows(), 1); + assert_eq!(batch.num_columns(), 3); + + let actual_user = batch.column(0).as_string::().value(0); + assert_eq!( + actual_user.to_uppercase(), + expected_user.to_uppercase(), + "CURRENT_USER() mismatch" + ); + + if let Some(wh) = expected_warehouse { + let actual_wh = batch.column(1).as_string::().value(0); + assert_eq!( + actual_wh.to_uppercase(), + wh.to_uppercase(), + "CURRENT_WAREHOUSE() mismatch" + ); + } + + if let Some(role) = expected_role { + let actual_role = batch.column(2).as_string::().value(0); + assert_eq!( + actual_role.to_uppercase(), + role.to_uppercase(), + "CURRENT_ROLE() mismatch" + ); + } +} + #[test] fn test_select_one() { - let Some(mut conn) = make_connection() else { + let Some(mut conn) = make_private_key_connection() else { eprintln!("Skipping: SNOWFLAKE_TEST_ACCOUNT/USER/PASSWORD not set"); return; }; @@ -71,7 +157,6 @@ fn test_get_table_types() { eprintln!("Skipping: SNOWFLAKE_TEST_ACCOUNT/USER/PASSWORD not set"); return; }; - use arrow_array::cast::AsArray; let mut reader = conn.get_table_types().expect("get_table_types"); let batch = reader.next().expect("no batch").expect("batch error"); let types: Vec<&str> = batch From c9fa7ebc19dfc5e4d09d345fbc6d5401a1403e2f Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Fri, 20 Mar 2026 11:31:25 -0400 Subject: [PATCH 21/76] license headers --- rust/adbc-snowflake/Cargo.toml | 2 +- rust/adbc-snowflake/src/connection.rs | 22 ++++++++++++++++++++++ rust/adbc-snowflake/src/database.rs | 22 ++++++++++++++++++++++ rust/adbc-snowflake/src/driver.rs | 22 ++++++++++++++++++++++ rust/adbc-snowflake/src/error.rs | 22 ++++++++++++++++++++++ rust/adbc-snowflake/src/lib.rs | 22 ++++++++++++++++++++++ rust/adbc-snowflake/src/statement.rs | 22 ++++++++++++++++++++++ rust/adbc-snowflake/tests/integration.rs | 22 ++++++++++++++++++++++ 8 files changed, 155 insertions(+), 1 deletion(-) diff --git a/rust/adbc-snowflake/Cargo.toml b/rust/adbc-snowflake/Cargo.toml index 1d9e6d2..e9f2114 100644 --- a/rust/adbc-snowflake/Cargo.toml +++ b/rust/adbc-snowflake/Cargo.toml @@ -1,4 +1,4 @@ -# Copyright (c) 2025 ADBC Drivers Contributors +# Copyright (c) 2026 ADBC Drivers Contributors # # This file has been modified from its original version, which is # under the Apache License: diff --git a/rust/adbc-snowflake/src/connection.rs b/rust/adbc-snowflake/src/connection.rs index 7a8667e..56f0e47 100644 --- a/rust/adbc-snowflake/src/connection.rs +++ b/rust/adbc-snowflake/src/connection.rs @@ -1,3 +1,25 @@ +// Copyright (c) 2026 ADBC Drivers Contributors +// +// This file has been modified from its original version, which is +// under the Apache License: +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + // src/connection.rs use std::collections::HashSet; use std::sync::Arc; diff --git a/rust/adbc-snowflake/src/database.rs b/rust/adbc-snowflake/src/database.rs index 0e5857e..ea2ddeb 100644 --- a/rust/adbc-snowflake/src/database.rs +++ b/rust/adbc-snowflake/src/database.rs @@ -1,3 +1,25 @@ +// Copyright (c) 2026 ADBC Drivers Contributors +// +// This file has been modified from its original version, which is +// under the Apache License: +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + // src/database.rs use std::collections::HashMap; use std::sync::Arc; diff --git a/rust/adbc-snowflake/src/driver.rs b/rust/adbc-snowflake/src/driver.rs index 54229eb..7cfddab 100644 --- a/rust/adbc-snowflake/src/driver.rs +++ b/rust/adbc-snowflake/src/driver.rs @@ -1,3 +1,25 @@ +// Copyright (c) 2026 ADBC Drivers Contributors +// +// This file has been modified from its original version, which is +// under the Apache License: +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + // src/driver.rs use std::sync::Arc; diff --git a/rust/adbc-snowflake/src/error.rs b/rust/adbc-snowflake/src/error.rs index aad2656..155fb02 100644 --- a/rust/adbc-snowflake/src/error.rs +++ b/rust/adbc-snowflake/src/error.rs @@ -1,3 +1,25 @@ +// Copyright (c) 2026 ADBC Drivers Contributors +// +// This file has been modified from its original version, which is +// under the Apache License: +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + // src/error.rs use adbc_core::error::{Error, Status}; use sf_core::apis::database_driver_v1::ApiError; diff --git a/rust/adbc-snowflake/src/lib.rs b/rust/adbc-snowflake/src/lib.rs index 0d385b9..17827ab 100644 --- a/rust/adbc-snowflake/src/lib.rs +++ b/rust/adbc-snowflake/src/lib.rs @@ -1,3 +1,25 @@ +// Copyright (c) 2026 ADBC Drivers Contributors +// +// This file has been modified from its original version, which is +// under the Apache License: +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + // src/lib.rs mod error; diff --git a/rust/adbc-snowflake/src/statement.rs b/rust/adbc-snowflake/src/statement.rs index 56fe1e7..eeec750 100644 --- a/rust/adbc-snowflake/src/statement.rs +++ b/rust/adbc-snowflake/src/statement.rs @@ -1,3 +1,25 @@ +// Copyright (c) 2026 ADBC Drivers Contributors +// +// This file has been modified from its original version, which is +// under the Apache License: +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + // src/statement.rs use std::sync::Arc; diff --git a/rust/adbc-snowflake/tests/integration.rs b/rust/adbc-snowflake/tests/integration.rs index 7095990..6213e4a 100644 --- a/rust/adbc-snowflake/tests/integration.rs +++ b/rust/adbc-snowflake/tests/integration.rs @@ -1,3 +1,25 @@ +// Copyright (c) 2026 ADBC Drivers Contributors +// +// This file has been modified from its original version, which is +// under the Apache License: +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + // tests/integration.rs use adbc_core::{ Connection as _, Database as _, Driver as _, Optionable, Statement as _, From 89f48a2e6c4380c85916a60d882764cd50129e5e Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Fri, 20 Mar 2026 11:46:58 -0400 Subject: [PATCH 22/76] update tests and implement get_info --- rust/adbc-snowflake/src/connection.rs | 59 ++++++- rust/adbc-snowflake/src/database.rs | 6 + rust/adbc-snowflake/tests/integration.rs | 186 +++++++++++------------ 3 files changed, 146 insertions(+), 105 deletions(-) diff --git a/rust/adbc-snowflake/src/connection.rs b/rust/adbc-snowflake/src/connection.rs index 56f0e47..999ba34 100644 --- a/rust/adbc-snowflake/src/connection.rs +++ b/rust/adbc-snowflake/src/connection.rs @@ -105,6 +105,40 @@ impl Connection { .map_err(crate::error::api_error_to_adbc_error) } + fn query_scalar(&self, sql: &str) -> Result { + let stmt_handle = self + .inner + .sf + .statement_new(self.conn_handle) + .map_err(crate::error::api_error_to_adbc_error)?; + let result = self.inner.runtime.block_on(async { + self.inner + .sf + .statement_set_sql_query(stmt_handle, sql.to_string()) + .await?; + self.inner + .sf + .statement_execute_query(stmt_handle, None) + .await + }); + let _ = self.inner.sf.statement_release(stmt_handle); + let exec_result = result.map_err(crate::error::api_error_to_adbc_error)?; + + let raw = Box::into_raw(exec_result.stream) + as *mut arrow_array::ffi_stream::FFI_ArrowArrayStream; + let mut reader = unsafe { arrow_array::ffi_stream::ArrowArrayStreamReader::from_raw(raw) } + .map_err(|e| Error::with_message_and_status(e.to_string(), Status::IO))?; + + use arrow_array::cast::AsArray; + let batch = reader + .next() + .ok_or_else(|| { + Error::with_message_and_status("empty result from scalar query", Status::IO) + })? + .map_err(|e| Error::with_message_and_status(e.to_string(), Status::IO))?; + Ok(batch.column(0).as_string::().value(0).to_string()) + } + pub(crate) fn set_autocommit(&mut self, enabled: bool) -> Result<()> { if enabled { if self.active_transaction { @@ -170,10 +204,17 @@ impl Optionable for Connection { } fn get_option_string(&self, key: Self::Option) -> Result { - Err(Error::with_message_and_status( - format!("option not found: {}", key.as_ref()), - Status::NotFound, - )) + match key { + OptionConnection::AutoCommit => { + Ok(if self.autocommit { "true" } else { "false" }.to_string()) + } + OptionConnection::CurrentCatalog => self.query_scalar("SELECT CURRENT_DATABASE()"), + OptionConnection::CurrentSchema => self.query_scalar("SELECT CURRENT_SCHEMA()"), + _ => Err(Error::with_message_and_status( + format!("option not found: {}", key.as_ref()), + Status::NotFound, + )), + } } fn get_option_bytes(&self, _key: Self::Option) -> Result> { @@ -227,6 +268,14 @@ impl adbc_core::Connection for Connection { &self, codes: Option>, ) -> Result> { + let need_vendor_version = + codes.as_ref().map_or(true, |s| s.contains(&InfoCode::VendorVersion)); + let vendor_version = if need_vendor_version { + self.query_scalar("SELECT CURRENT_VERSION()")? + } else { + String::new() + }; + // (InfoCode, type_id, offset_within_arm_array) let all_entries: &[(InfoCode, i8, i32)] = &[ (InfoCode::VendorName, 0, 0), @@ -235,6 +284,7 @@ impl adbc_core::Connection for Connection { (InfoCode::DriverName, 0, 1), (InfoCode::DriverVersion, 0, 2), (InfoCode::DriverAdbcVersion, 2, 0), + (InfoCode::VendorVersion, 0, 3), ]; let selected: Vec<_> = match &codes { @@ -260,6 +310,7 @@ impl adbc_core::Connection for Connection { "Snowflake", "ADBC Snowflake Driver (Rust)", env!("CARGO_PKG_VERSION"), + vendor_version.as_str(), ])) as ArrayRef; let bool_values = Arc::new(BooleanArray::from(vec![true, false])) as ArrayRef; let int64_values = diff --git a/rust/adbc-snowflake/src/database.rs b/rust/adbc-snowflake/src/database.rs index ea2ddeb..026702c 100644 --- a/rust/adbc-snowflake/src/database.rs +++ b/rust/adbc-snowflake/src/database.rs @@ -271,6 +271,12 @@ impl Database { "port" => "adbc.snowflake.sql.uri.port", "protocol" => "adbc.snowflake.sql.uri.protocol", "authenticator" => "adbc.snowflake.sql.auth_type", + "private_key_file" => { + "adbc.snowflake.sql.client_option.jwt_private_key" + } + "private_key" => { + "adbc.snowflake.sql.client_option.jwt_private_key_pkcs8_value" + } _ => continue, }; self.set_option( diff --git a/rust/adbc-snowflake/tests/integration.rs b/rust/adbc-snowflake/tests/integration.rs index 6213e4a..abba1ee 100644 --- a/rust/adbc-snowflake/tests/integration.rs +++ b/rust/adbc-snowflake/tests/integration.rs @@ -23,7 +23,7 @@ // tests/integration.rs use adbc_core::{ Connection as _, Database as _, Driver as _, Optionable, Statement as _, - options::{OptionDatabase, OptionValue}, + options::{OptionConnection, OptionDatabase, OptionValue}, }; use adbc_snowflake::Driver; use arrow_array::cast::AsArray; @@ -33,97 +33,35 @@ fn get_env(key: &str) -> Option { } fn make_connection() -> Option { - let account = get_env("SNOWFLAKE_TEST_ACCOUNT")?; - let user = get_env("SNOWFLAKE_TEST_USER")?; - + let uri = get_env("SNOWFLAKE_URI")?; let mut driver = Driver::default(); let mut db = driver.new_database().expect("new_database"); - db.set_option( - OptionDatabase::Other("adbc.snowflake.sql.account".into()), - OptionValue::String(account), - ) - .expect("set account"); - db.set_option(OptionDatabase::Username, OptionValue::String(user)) - .expect("set user"); - - if let Some(wh) = get_env("SNOWFLAKE_TEST_WAREHOUSE") { - db.set_option( - OptionDatabase::Other("adbc.snowflake.sql.warehouse".into()), - OptionValue::String(wh), - ) - .expect("set warehouse"); - } - if let Some(db_name) = get_env("SNOWFLAKE_TEST_DATABASE") { + db.set_option(OptionDatabase::Uri, OptionValue::String(uri)) + .expect("set uri"); + if let Some(database) = get_env("SNOWFLAKE_DATABASE") { db.set_option( OptionDatabase::Other("adbc.snowflake.sql.db".into()), - OptionValue::String(db_name), + OptionValue::String(database), ) .expect("set database"); } - if let Some(schema) = get_env("SNOWFLAKE_TEST_SCHEMA") { + if let Some(schema) = get_env("SNOWFLAKE_SCHEMA") { db.set_option( OptionDatabase::Other("adbc.snowflake.sql.schema".into()), OptionValue::String(schema), ) .expect("set schema"); } - - Some(db.new_connection().expect("new_connection")) -} - -fn make_private_key_connection() -> Option { - let account = get_env("SNOWFLAKE_TEST_ACCOUNT")?; - let user = get_env("SNOWFLAKE_TEST_USER")?; - let private_key_file = get_env("SNOWFLAKE_TEST_PRIVATE_KEY_FILE")?; - - let mut driver = Driver::default(); - let mut db = driver.new_database().expect("new_database"); - db.set_option( - OptionDatabase::Other("adbc.snowflake.sql.account".into()), - OptionValue::String(account), - ) - .expect("set account"); - db.set_option(OptionDatabase::Username, OptionValue::String(user)) - .expect("set user"); - db.set_option( - OptionDatabase::Other("adbc.snowflake.sql.auth_type".into()), - OptionValue::String("SNOWFLAKE_JWT".into()), - ) - .expect("set auth_type"); - db.set_option( - OptionDatabase::Other("adbc.snowflake.sql.client_option.jwt_private_key".into()), - OptionValue::String(private_key_file), - ) - .expect("set private_key_file"); - if let Some(wh) = get_env("SNOWFLAKE_TEST_WAREHOUSE") { - db.set_option( - OptionDatabase::Other("adbc.snowflake.sql.warehouse".into()), - OptionValue::String(wh), - ) - .expect("set warehouse"); - } - if let Some(role) = get_env("SNOWFLAKE_TEST_ROLE") { - db.set_option( - OptionDatabase::Other("adbc.snowflake.sql.role".into()), - OptionValue::String(role), - ) - .expect("set role"); - } - Some(db.new_connection().expect("new_connection")) } #[test] fn test_private_key_simple_query() { - let Some(mut conn) = make_private_key_connection() else { - eprintln!("Skipping: SNOWFLAKE_TEST_ACCOUNT/USER/PRIVATE_KEY_FILE not set"); + let Some(mut conn) = make_connection() else { + eprintln!("Skipping: SNOWFLAKE_URI not set"); return; }; - let expected_user = get_env("SNOWFLAKE_TEST_USER").unwrap(); - let expected_warehouse = get_env("SNOWFLAKE_TEST_WAREHOUSE"); - let expected_role = get_env("SNOWFLAKE_TEST_ROLE"); - let mut stmt = conn.new_statement().expect("new_statement"); stmt.set_sql_query("SELECT CURRENT_USER(), CURRENT_WAREHOUSE(), CURRENT_ROLE()") .expect("set_sql_query"); @@ -132,37 +70,15 @@ fn test_private_key_simple_query() { assert_eq!(batch.num_rows(), 1); assert_eq!(batch.num_columns(), 3); - - let actual_user = batch.column(0).as_string::().value(0); - assert_eq!( - actual_user.to_uppercase(), - expected_user.to_uppercase(), - "CURRENT_USER() mismatch" - ); - - if let Some(wh) = expected_warehouse { - let actual_wh = batch.column(1).as_string::().value(0); - assert_eq!( - actual_wh.to_uppercase(), - wh.to_uppercase(), - "CURRENT_WAREHOUSE() mismatch" - ); - } - - if let Some(role) = expected_role { - let actual_role = batch.column(2).as_string::().value(0); - assert_eq!( - actual_role.to_uppercase(), - role.to_uppercase(), - "CURRENT_ROLE() mismatch" - ); - } + assert!(!batch.column(0).as_string::().value(0).is_empty(), "CURRENT_USER() is empty"); + assert!(!batch.column(1).as_string::().value(0).is_empty(), "CURRENT_WAREHOUSE() is empty"); + assert!(!batch.column(2).as_string::().value(0).is_empty(), "CURRENT_ROLE() is empty"); } #[test] fn test_select_one() { - let Some(mut conn) = make_private_key_connection() else { - eprintln!("Skipping: SNOWFLAKE_TEST_ACCOUNT/USER/PASSWORD not set"); + let Some(mut conn) = make_connection() else { + eprintln!("Skipping: SNOWFLAKE_URI not set"); return; }; let mut stmt = conn.new_statement().expect("new_statement"); @@ -176,7 +92,7 @@ fn test_select_one() { #[test] fn test_get_table_types() { let Some(conn) = make_connection() else { - eprintln!("Skipping: SNOWFLAKE_TEST_ACCOUNT/USER/PASSWORD not set"); + eprintln!("Skipping: SNOWFLAKE_URI not set"); return; }; let mut reader = conn.get_table_types().expect("get_table_types"); @@ -194,7 +110,7 @@ fn test_get_table_types() { #[test] fn test_get_info_no_codes() { let Some(conn) = make_connection() else { - eprintln!("Skipping: SNOWFLAKE_TEST_ACCOUNT/USER/PASSWORD not set"); + eprintln!("Skipping: SNOWFLAKE_URI not set"); return; }; let mut reader = conn.get_info(None).expect("get_info"); @@ -202,10 +118,78 @@ fn test_get_info_no_codes() { assert!(batch.num_rows() > 0, "expected at least one info row"); } +#[test] +fn test_get_info_vendor_version() { + use adbc_core::options::InfoCode; + use std::collections::HashSet; + + let Some(conn) = make_connection() else { + eprintln!("Skipping: SNOWFLAKE_URI not set"); + return; + }; + + let mut reader = conn + .get_info(Some(HashSet::from([InfoCode::VendorVersion]))) + .expect("get_info"); + let batch = reader.next().expect("no batch").expect("batch error"); + assert_eq!(batch.num_rows(), 1); + + // VendorVersion is a string value — type_id 0 in the union + use arrow_array::cast::AsArray; + let type_ids = batch.column(1).as_any().downcast_ref::().unwrap(); + assert_eq!(type_ids.type_id(0), 0, "VendorVersion should be a string union arm"); + let version_str = type_ids.value(0); + let v = version_str.as_string::().value(0); + assert!(!v.is_empty(), "VendorVersion should not be empty"); + // Snowflake versions look like "8.x.x" — just check it contains a dot + assert!(v.contains('.'), "VendorVersion should look like a version string, got: {v}"); +} + +#[test] +fn test_get_option_string_current_catalog_and_schema() { + let Some(conn) = make_connection() else { + eprintln!("Skipping: SNOWFLAKE_URI not set"); + return; + }; + + let catalog = conn + .get_option_string(OptionConnection::CurrentCatalog) + .expect("get CurrentCatalog"); + let schema = conn + .get_option_string(OptionConnection::CurrentSchema) + .expect("get CurrentSchema"); + + assert!(!catalog.is_empty(), "current catalog should not be empty"); + assert!(!schema.is_empty(), "current schema should not be empty"); +} + +#[test] +fn test_get_option_string_autocommit() { + let Some(mut conn) = make_connection() else { + eprintln!("Skipping: SNOWFLAKE_URI not set"); + return; + }; + + let ac = conn + .get_option_string(OptionConnection::AutoCommit) + .expect("get AutoCommit"); + assert_eq!(ac, "true", "default autocommit should be true"); + + conn.set_option( + OptionConnection::AutoCommit, + OptionValue::String("false".into()), + ) + .expect("disable autocommit"); + let ac = conn + .get_option_string(OptionConnection::AutoCommit) + .expect("get AutoCommit after disable"); + assert_eq!(ac, "false"); +} + #[test] fn test_execute_ddl_and_dml() { let Some(mut conn) = make_connection() else { - eprintln!("Skipping: SNOWFLAKE_TEST_ACCOUNT/USER/PASSWORD not set"); + eprintln!("Skipping: SNOWFLAKE_URI not set"); return; }; From 06b14d8e9c122c4783b6cc97d7dce91f5b2c8cf3 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Fri, 20 Mar 2026 11:56:33 -0400 Subject: [PATCH 23/76] add precision options --- rust/adbc-snowflake/src/connection.rs | 97 +++++++++++++++++++++------ rust/adbc-snowflake/src/database.rs | 41 ++++++++++- rust/adbc-snowflake/src/driver.rs | 24 +++++++ rust/adbc-snowflake/src/statement.rs | 32 ++++++++- 4 files changed, 173 insertions(+), 21 deletions(-) diff --git a/rust/adbc-snowflake/src/connection.rs b/rust/adbc-snowflake/src/connection.rs index 999ba34..f53cbbf 100644 --- a/rust/adbc-snowflake/src/connection.rs +++ b/rust/adbc-snowflake/src/connection.rs @@ -38,7 +38,7 @@ use arrow_buffer::ScalarBuffer; use arrow_schema::{DataType, Field, Schema}; use sf_core::apis::database_driver_v1::Handle; -use crate::driver::Inner; +use crate::driver::{Inner, TimestampPrecision}; use crate::statement::Statement; pub struct Connection { @@ -46,6 +46,8 @@ pub struct Connection { pub(crate) conn_handle: Handle, pub(crate) autocommit: bool, pub(crate) active_transaction: bool, + pub(crate) use_high_precision: bool, + pub(crate) timestamp_precision: TimestampPrecision, } impl Drop for Connection { @@ -256,6 +258,8 @@ impl adbc_core::Connection for Connection { target_table: None, ingest_mode: None, query_tag: None, + use_high_precision: self.use_high_precision, + timestamp_precision: self.timestamp_precision, }) } @@ -474,7 +478,11 @@ impl adbc_core::Connection for Connection { let types = batch.column(1).as_string::(); let nullables = batch.column(3).as_string::(); for i in 0..batch.num_rows() { - let arrow_type = snowflake_type_to_arrow(types.value(i)); + let arrow_type = snowflake_type_to_arrow( + types.value(i), + self.use_high_precision, + self.timestamp_precision.time_unit(), + ); fields.push(Field::new( names.value(i), arrow_type, @@ -538,7 +546,11 @@ impl adbc_core::Connection for Connection { } } -fn snowflake_type_to_arrow(type_str: &str) -> DataType { +fn snowflake_type_to_arrow( + type_str: &str, + high_precision: bool, + ts_unit: arrow_schema::TimeUnit, +) -> DataType { let upper = type_str.to_uppercase(); let base = upper.split('(').next().unwrap_or(&upper).trim(); match base { @@ -556,13 +568,19 @@ fn snowflake_type_to_arrow(type_str: &str) -> DataType { .find('(') .and_then(|s| type_str.rfind(')').map(|e| &type_str[s + 1..e])) { - let scale = inner - .split(',') - .nth(1) - .and_then(|s| s.trim().parse::().ok()) + let mut parts = inner.split(','); + let precision = parts + .next() + .and_then(|s| s.trim().parse::().ok()) + .unwrap_or(38); + let scale = parts + .next() + .and_then(|s| s.trim().parse::().ok()) .unwrap_or(0); if scale == 0 { DataType::Int64 + } else if high_precision { + DataType::Decimal128(precision, scale) } else { DataType::Float64 } @@ -570,11 +588,9 @@ fn snowflake_type_to_arrow(type_str: &str) -> DataType { DataType::Int64 } } - "TIMESTAMP" | "TIMESTAMP_NTZ" | "DATETIME" => { - DataType::Timestamp(arrow_schema::TimeUnit::Nanosecond, None) - } + "TIMESTAMP" | "TIMESTAMP_NTZ" | "DATETIME" => DataType::Timestamp(ts_unit, None), "TIMESTAMP_LTZ" | "TIMESTAMP_TZ" => { - DataType::Timestamp(arrow_schema::TimeUnit::Nanosecond, Some("UTC".into())) + DataType::Timestamp(ts_unit, Some("UTC".into())) } _ => DataType::Utf8, } @@ -592,6 +608,8 @@ mod tests { conn_handle: sf_core::apis::database_driver_v1::Handle { id: 0, magic: 0 }, autocommit: true, active_transaction: false, + use_high_precision: true, + timestamp_precision: TimestampPrecision::Nanoseconds, }; let result = conn.get_option_string(OptionConnection::Other("unknown".into())); assert_eq!(result.unwrap_err().status, Status::NotFound); @@ -599,33 +617,72 @@ mod tests { #[test] fn snowflake_type_number_no_scale_is_int64() { - assert_eq!(snowflake_type_to_arrow("NUMBER(38,0)"), DataType::Int64); + assert_eq!( + snowflake_type_to_arrow("NUMBER(38,0)", true, arrow_schema::TimeUnit::Nanosecond), + DataType::Int64 + ); + } + + #[test] + fn snowflake_type_number_with_scale_high_precision_is_decimal128() { + assert_eq!( + snowflake_type_to_arrow("NUMBER(10,2)", true, arrow_schema::TimeUnit::Nanosecond), + DataType::Decimal128(10, 2) + ); } #[test] - fn snowflake_type_number_with_scale_is_float64() { - assert_eq!(snowflake_type_to_arrow("NUMBER(10,2)"), DataType::Float64); + fn snowflake_type_number_with_scale_low_precision_is_float64() { + assert_eq!( + snowflake_type_to_arrow("NUMBER(10,2)", false, arrow_schema::TimeUnit::Nanosecond), + DataType::Float64 + ); } #[test] fn snowflake_type_text_is_utf8() { - assert_eq!(snowflake_type_to_arrow("TEXT"), DataType::Utf8); - assert_eq!(snowflake_type_to_arrow("VARCHAR(16777216)"), DataType::Utf8); + assert_eq!( + snowflake_type_to_arrow("TEXT", true, arrow_schema::TimeUnit::Nanosecond), + DataType::Utf8 + ); + assert_eq!( + snowflake_type_to_arrow("VARCHAR(16777216)", true, arrow_schema::TimeUnit::Nanosecond), + DataType::Utf8 + ); } #[test] fn snowflake_type_boolean_is_boolean() { - assert_eq!(snowflake_type_to_arrow("BOOLEAN"), DataType::Boolean); + assert_eq!( + snowflake_type_to_arrow("BOOLEAN", true, arrow_schema::TimeUnit::Nanosecond), + DataType::Boolean + ); } #[test] - fn snowflake_type_timestamp_ntz_is_nanosecond() { + fn snowflake_type_timestamp_ntz_nanosecond() { assert_eq!( - snowflake_type_to_arrow("TIMESTAMP_NTZ(9)"), + snowflake_type_to_arrow( + "TIMESTAMP_NTZ(9)", + true, + arrow_schema::TimeUnit::Nanosecond + ), DataType::Timestamp(arrow_schema::TimeUnit::Nanosecond, None) ); } + #[test] + fn snowflake_type_timestamp_ntz_microsecond() { + assert_eq!( + snowflake_type_to_arrow( + "TIMESTAMP_NTZ(6)", + true, + arrow_schema::TimeUnit::Microsecond + ), + DataType::Timestamp(arrow_schema::TimeUnit::Microsecond, None) + ); + } + #[test] fn get_table_types_returns_table_and_view() { use adbc_core::Connection as _; @@ -636,6 +693,8 @@ mod tests { conn_handle: sf_core::apis::database_driver_v1::Handle { id: 0, magic: 0 }, autocommit: true, active_transaction: false, + use_high_precision: true, + timestamp_precision: TimestampPrecision::Nanoseconds, }; let mut reader = conn.get_table_types().unwrap(); let batch = reader.next().unwrap().unwrap(); diff --git a/rust/adbc-snowflake/src/database.rs b/rust/adbc-snowflake/src/database.rs index 026702c..db98926 100644 --- a/rust/adbc-snowflake/src/database.rs +++ b/rust/adbc-snowflake/src/database.rs @@ -34,7 +34,7 @@ use sf_core::config::param_registry::param_names; use sf_core::config::settings::Setting; use crate::connection::Connection; -use crate::driver::Inner; +use crate::driver::{Inner, TimestampPrecision}; /// Convert an ADBC OptionDatabase key + OptionValue into an sf_core (param_name, Setting) pair. /// Returns None for the "uri" key (handled by apply_uri separately). @@ -104,6 +104,10 @@ pub struct Database { /// Local copy of sf_core settings keyed by canonical param name. /// Propagated to each new connection before connection_init. pub(crate) sf_settings: HashMap, + /// Map NUMBER(p,s) with s>0 to Decimal128 instead of Float64. + pub(crate) use_high_precision: bool, + /// Arrow time unit used for TIMESTAMP columns. + pub(crate) timestamp_precision: TimestampPrecision, } impl Drop for Database { @@ -126,6 +130,24 @@ impl Optionable for Database { Status::InvalidArguments, )); } + if key_str == "adbc.snowflake.sql.client_option.use_high_precision" { + if let OptionValue::String(s) = &value { + self.use_high_precision = s == "enabled" || s == "true"; + } + return Ok(()); + } + if key_str == "adbc.snowflake.sql.client_option.max_timestamp_precision" { + if let OptionValue::String(s) = &value { + self.timestamp_precision = match s.as_str() { + "microseconds" => TimestampPrecision::Microseconds, + "nanoseconds_error_on_overflow" => { + TimestampPrecision::NanosecondsErrorOnOverflow + } + _ => TimestampPrecision::Nanoseconds, + }; + } + return Ok(()); + } if let Some((param, setting)) = adbc_db_opt_to_sf(key_str, &value)? { self.sf_settings.insert(param.clone(), setting.clone()); self.inner @@ -142,6 +164,21 @@ impl Optionable for Database { fn get_option_string(&self, key: Self::Option) -> Result { let key_str = key.as_ref(); + if key_str == "adbc.snowflake.sql.client_option.use_high_precision" { + return Ok(if self.use_high_precision { + "enabled".to_string() + } else { + "disabled".to_string() + }); + } + if key_str == "adbc.snowflake.sql.client_option.max_timestamp_precision" { + return Ok(match self.timestamp_precision { + TimestampPrecision::Microseconds => "microseconds", + TimestampPrecision::NanosecondsErrorOnOverflow => "nanoseconds_error_on_overflow", + TimestampPrecision::Nanoseconds => "nanoseconds", + } + .to_string()); + } if let Ok(Some((param, _))) = adbc_db_opt_to_sf(key_str, &OptionValue::String(String::new())) && let Some(Setting::String(s)) = self.sf_settings.get(¶m) @@ -392,6 +429,8 @@ impl adbc_core::Database for Database { conn_handle, autocommit: true, active_transaction: false, + use_high_precision: self.use_high_precision, + timestamp_precision: self.timestamp_precision, }; if let Some(ac) = post_autocommit { diff --git a/rust/adbc-snowflake/src/driver.rs b/rust/adbc-snowflake/src/driver.rs index 7cfddab..a26a016 100644 --- a/rust/adbc-snowflake/src/driver.rs +++ b/rust/adbc-snowflake/src/driver.rs @@ -28,11 +28,33 @@ use adbc_core::{ error::{Error, Result, Status}, options::{OptionDatabase, OptionValue}, }; +use arrow_schema::TimeUnit; use sf_core::apis::database_driver_v1::DatabaseDriverV1; use tokio::runtime::Runtime; use crate::database::Database; +/// Controls the Arrow time unit used for Snowflake TIMESTAMP columns. +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub(crate) enum TimestampPrecision { + /// Nanosecond precision (default). May overflow for dates outside 1677–2262. + #[default] + Nanoseconds, + /// Microsecond precision. Safe for all Snowflake-representable dates. + Microseconds, + /// Nanosecond precision; returns an error when a value would overflow. + NanosecondsErrorOnOverflow, +} + +impl TimestampPrecision { + pub(crate) fn time_unit(self) -> TimeUnit { + match self { + Self::Microseconds => TimeUnit::Microsecond, + _ => TimeUnit::Nanosecond, + } + } +} + pub(crate) struct Inner { pub runtime: Runtime, pub sf: DatabaseDriverV1, @@ -90,6 +112,8 @@ impl adbc_core::Driver for Driver { inner: self.inner.clone(), db_handle, sf_settings, + use_high_precision: true, + timestamp_precision: TimestampPrecision::default(), }; for (key, value) in opts { db.set_option(key, value)?; diff --git a/rust/adbc-snowflake/src/statement.rs b/rust/adbc-snowflake/src/statement.rs index eeec750..402fd15 100644 --- a/rust/adbc-snowflake/src/statement.rs +++ b/rust/adbc-snowflake/src/statement.rs @@ -32,7 +32,7 @@ use arrow_array::{RecordBatch, RecordBatchReader}; use arrow_schema::Schema; use sf_core::apis::database_driver_v1::Handle; -use crate::driver::Inner; +use crate::driver::{Inner, TimestampPrecision}; pub struct Statement { pub(crate) inner: Arc, @@ -42,6 +42,8 @@ pub struct Statement { pub(crate) target_table: Option, pub(crate) ingest_mode: Option, pub(crate) query_tag: Option, + pub(crate) use_high_precision: bool, + pub(crate) timestamp_precision: TimestampPrecision, } impl Drop for Statement { @@ -78,6 +80,28 @@ impl Optionable for Statement { )) } } + OptionStatement::Other(ref k) + if k == "adbc.snowflake.sql.client_option.use_high_precision" => + { + if let OptionValue::String(s) = value { + self.use_high_precision = s == "enabled" || s == "true"; + } + Ok(()) + } + OptionStatement::Other(ref k) + if k == "adbc.snowflake.sql.client_option.max_timestamp_precision" => + { + if let OptionValue::String(s) = value { + self.timestamp_precision = match s.as_str() { + "microseconds" => TimestampPrecision::Microseconds, + "nanoseconds_error_on_overflow" => { + TimestampPrecision::NanosecondsErrorOnOverflow + } + _ => TimestampPrecision::Nanoseconds, + }; + } + Ok(()) + } OptionStatement::Other(ref k) if k == "adbc.snowflake.statement.query_tag" => { if let OptionValue::String(s) = value { self.query_tag = Some(s); @@ -287,6 +311,8 @@ mod tests { target_table: None, ingest_mode: None, query_tag: None, + use_high_precision: true, + timestamp_precision: TimestampPrecision::Nanoseconds, } } @@ -317,6 +343,8 @@ mod tests { target_table: Some("mytable".into()), ingest_mode: None, query_tag: None, + use_high_precision: true, + timestamp_precision: TimestampPrecision::Nanoseconds, }; match stmt.execute() { Err(err) => assert_eq!(err.status, adbc_core::error::Status::NotImplemented), @@ -335,6 +363,8 @@ mod tests { target_table: Some("mytable".into()), ingest_mode: None, query_tag: None, + use_high_precision: true, + timestamp_precision: TimestampPrecision::Nanoseconds, }; stmt.set_sql_query("SELECT 1").unwrap(); assert!(stmt.target_table.is_none()); From 9b1803986254ea4b34f7b010061ea383eab13f9b Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Fri, 20 Mar 2026 12:01:05 -0400 Subject: [PATCH 24/76] fix tests --- rust/adbc-snowflake/tests/integration.rs | 238 ++++++++++++++++++++++- 1 file changed, 235 insertions(+), 3 deletions(-) diff --git a/rust/adbc-snowflake/tests/integration.rs b/rust/adbc-snowflake/tests/integration.rs index abba1ee..872cabc 100644 --- a/rust/adbc-snowflake/tests/integration.rs +++ b/rust/adbc-snowflake/tests/integration.rs @@ -25,14 +25,17 @@ use adbc_core::{ Connection as _, Database as _, Driver as _, Optionable, Statement as _, options::{OptionConnection, OptionDatabase, OptionValue}, }; -use adbc_snowflake::Driver; +use adbc_snowflake::{Database, Driver}; use arrow_array::cast::AsArray; +use arrow_schema::{DataType, TimeUnit}; fn get_env(key: &str) -> Option { std::env::var(key).ok().filter(|s| !s.is_empty()) } -fn make_connection() -> Option { +/// Build a configured Database without opening a connection. +/// Callers can set extra options before calling `.new_connection()`. +fn make_db() -> Option { let uri = get_env("SNOWFLAKE_URI")?; let mut driver = Driver::default(); let mut db = driver.new_database().expect("new_database"); @@ -52,7 +55,11 @@ fn make_connection() -> Option { ) .expect("set schema"); } - Some(db.new_connection().expect("new_connection")) + Some(db) +} + +fn make_connection() -> Option { + Some(make_db()?.new_connection().expect("new_connection")) } #[test] @@ -223,3 +230,228 @@ fn test_execute_ddl_and_dml() { stmt.execute_update().expect("drop table"); } } + +// ── precision option tests ──────────────────────────────────────────────────── + +/// Verify that the precision options round-trip correctly on Database +/// (no live query required — purely option set/get). +#[test] +fn test_precision_options_defaults_and_round_trip() { + let Some(mut db) = make_db() else { + eprintln!("Skipping: SNOWFLAKE_URI not set"); + return; + }; + + // Defaults + assert_eq!( + db.get_option_string(OptionDatabase::Other( + "adbc.snowflake.sql.client_option.use_high_precision".into() + )) + .unwrap(), + "enabled" + ); + assert_eq!( + db.get_option_string(OptionDatabase::Other( + "adbc.snowflake.sql.client_option.max_timestamp_precision".into() + )) + .unwrap(), + "nanoseconds" + ); + + // Round-trip: disable high precision + db.set_option( + OptionDatabase::Other("adbc.snowflake.sql.client_option.use_high_precision".into()), + OptionValue::String("disabled".into()), + ) + .unwrap(); + assert_eq!( + db.get_option_string(OptionDatabase::Other( + "adbc.snowflake.sql.client_option.use_high_precision".into() + )) + .unwrap(), + "disabled" + ); + + // Round-trip: microsecond timestamps + db.set_option( + OptionDatabase::Other( + "adbc.snowflake.sql.client_option.max_timestamp_precision".into(), + ), + OptionValue::String("microseconds".into()), + ) + .unwrap(); + assert_eq!( + db.get_option_string(OptionDatabase::Other( + "adbc.snowflake.sql.client_option.max_timestamp_precision".into() + )) + .unwrap(), + "microseconds" + ); + + // Round-trip: nanoseconds_error_on_overflow + db.set_option( + OptionDatabase::Other( + "adbc.snowflake.sql.client_option.max_timestamp_precision".into(), + ), + OptionValue::String("nanoseconds_error_on_overflow".into()), + ) + .unwrap(); + assert_eq!( + db.get_option_string(OptionDatabase::Other( + "adbc.snowflake.sql.client_option.max_timestamp_precision".into() + )) + .unwrap(), + "nanoseconds_error_on_overflow" + ); +} + +/// Mirrors Go's TestUseHighPrecision / TestSchemaWithLowPrecision. +/// +/// With high precision (default): NUMBER(10,0) → Int64, NUMBER(15,2) → Decimal128(15,2). +/// With high precision disabled: NUMBER(10,0) → Int64, NUMBER(15,2) → Float64. +#[test] +fn test_high_precision_get_table_schema() { + let Some(mut conn) = make_connection() else { + eprintln!("Skipping: SNOWFLAKE_URI not set"); + return; + }; + + // Create a permanent table so a second connection can also DESC it. + { + let mut stmt = conn.new_statement().unwrap(); + stmt.set_sql_query( + "CREATE OR REPLACE TABLE adbc_rust_precision_test \ + (INT_COL NUMBER(10,0), DEC_COL NUMBER(15,2))", + ) + .unwrap(); + stmt.execute_update().expect("create precision test table"); + } + + // Snowflake folds unquoted identifiers to uppercase. + // ── high precision (default: enabled) ──────────────────────────────────── + let schema_hp = conn + .get_table_schema(None, None, "ADBC_RUST_PRECISION_TEST") + .expect("get_table_schema high precision"); + + assert_eq!( + schema_hp.field_with_name("INT_COL").unwrap().data_type(), + &DataType::Int64, + "INT_COL: expected Int64 (scale=0)" + ); + assert_eq!( + schema_hp.field_with_name("DEC_COL").unwrap().data_type(), + &DataType::Decimal128(15, 2), + "DEC_COL: expected Decimal128(15,2) with high precision" + ); + + // ── low precision (disabled) ────────────────────────────────────────────── + let Some(mut db_lp) = make_db() else { return }; + db_lp + .set_option( + OptionDatabase::Other( + "adbc.snowflake.sql.client_option.use_high_precision".into(), + ), + OptionValue::String("disabled".into()), + ) + .unwrap(); + let conn_lp = db_lp.new_connection().expect("low-precision connection"); + + let schema_lp = conn_lp + .get_table_schema(None, None, "ADBC_RUST_PRECISION_TEST") + .expect("get_table_schema low precision"); + + assert_eq!( + schema_lp.field_with_name("INT_COL").unwrap().data_type(), + &DataType::Int64, + "INT_COL: expected Int64 (scale=0)" + ); + assert_eq!( + schema_lp.field_with_name("DEC_COL").unwrap().data_type(), + &DataType::Float64, + "DEC_COL: expected Float64 with low precision" + ); + + // Cleanup + { + let mut stmt = conn.new_statement().unwrap(); + stmt.set_sql_query("DROP TABLE IF EXISTS adbc_rust_precision_test") + .unwrap(); + stmt.execute_update().expect("drop precision test table"); + } +} + +/// Mirrors Go's TestTimestampPrecision. +/// +/// With nanoseconds (default): TIMESTAMP_NTZ → Timestamp(Nanosecond, None), +/// TIMESTAMP_TZ → Timestamp(Nanosecond, UTC). +/// With microseconds: TIMESTAMP_NTZ → Timestamp(Microsecond, None), +/// TIMESTAMP_TZ → Timestamp(Microsecond, UTC). +#[test] +fn test_timestamp_precision_get_table_schema() { + let Some(mut conn) = make_connection() else { + eprintln!("Skipping: SNOWFLAKE_URI not set"); + return; + }; + + { + let mut stmt = conn.new_statement().unwrap(); + stmt.set_sql_query( + "CREATE OR REPLACE TABLE adbc_rust_ts_precision_test \ + (NTZ_COL TIMESTAMP_NTZ, TZ_COL TIMESTAMP_TZ)", + ) + .unwrap(); + stmt.execute_update().expect("create ts precision test table"); + } + + // Snowflake folds unquoted identifiers to uppercase. + // ── nanoseconds (default) ───────────────────────────────────────────────── + let schema_ns = conn + .get_table_schema(None, None, "ADBC_RUST_TS_PRECISION_TEST") + .expect("get_table_schema nanoseconds"); + + assert_eq!( + schema_ns.field_with_name("NTZ_COL").unwrap().data_type(), + &DataType::Timestamp(TimeUnit::Nanosecond, None), + "NTZ_COL: expected Timestamp(Nanosecond, None)" + ); + assert_eq!( + schema_ns.field_with_name("TZ_COL").unwrap().data_type(), + &DataType::Timestamp(TimeUnit::Nanosecond, Some("UTC".into())), + "TZ_COL: expected Timestamp(Nanosecond, UTC)" + ); + + // ── microseconds ────────────────────────────────────────────────────────── + let Some(mut db_us) = make_db() else { return }; + db_us + .set_option( + OptionDatabase::Other( + "adbc.snowflake.sql.client_option.max_timestamp_precision".into(), + ), + OptionValue::String("microseconds".into()), + ) + .unwrap(); + let conn_us = db_us.new_connection().expect("microsecond connection"); + + let schema_us = conn_us + .get_table_schema(None, None, "ADBC_RUST_TS_PRECISION_TEST") + .expect("get_table_schema microseconds"); + + assert_eq!( + schema_us.field_with_name("NTZ_COL").unwrap().data_type(), + &DataType::Timestamp(TimeUnit::Microsecond, None), + "NTZ_COL: expected Timestamp(Microsecond, None)" + ); + assert_eq!( + schema_us.field_with_name("TZ_COL").unwrap().data_type(), + &DataType::Timestamp(TimeUnit::Microsecond, Some("UTC".into())), + "TZ_COL: expected Timestamp(Microsecond, UTC)" + ); + + // Cleanup + { + let mut stmt = conn.new_statement().unwrap(); + stmt.set_sql_query("DROP TABLE IF EXISTS adbc_rust_ts_precision_test") + .unwrap(); + stmt.execute_update().expect("drop ts precision test table"); + } +} From abb51abe173146cffa9b8fd724c2872274335e44 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Fri, 20 Mar 2026 12:03:22 -0400 Subject: [PATCH 25/76] fix get_table_schema --- rust/adbc-snowflake/src/connection.rs | 32 ++++++++++++++++++++------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/rust/adbc-snowflake/src/connection.rs b/rust/adbc-snowflake/src/connection.rs index f53cbbf..ca0285b 100644 --- a/rust/adbc-snowflake/src/connection.rs +++ b/rust/adbc-snowflake/src/connection.rs @@ -31,8 +31,8 @@ use adbc_core::{ schemas, }; use arrow_array::{ - ArrayRef, BooleanArray, Int64Array, RecordBatch, RecordBatchReader, StringArray, UInt32Array, - UnionArray, + Array, ArrayRef, BooleanArray, Int64Array, RecordBatch, RecordBatchReader, StringArray, + UInt32Array, UnionArray, }; use arrow_buffer::ScalarBuffer; use arrow_schema::{DataType, Field, Schema}; @@ -477,17 +477,33 @@ impl adbc_core::Connection for Connection { let names = batch.column(0).as_string::(); let types = batch.column(1).as_string::(); let nullables = batch.column(3).as_string::(); + // primary_key is column 5; comment is column 9 — present only when + // the result has enough columns (older Snowflake editions may omit them). + let primary_keys = (batch.num_columns() > 5) + .then(|| batch.column(5).as_string::()); + let comments = (batch.num_columns() > 9) + .then(|| batch.column(9).as_string::()); for i in 0..batch.num_rows() { + let type_str = types.value(i); let arrow_type = snowflake_type_to_arrow( - types.value(i), + type_str, self.use_high_precision, self.timestamp_precision.time_unit(), ); - fields.push(Field::new( - names.value(i), - arrow_type, - nullables.value(i) == "Y", - )); + let mut md = std::collections::HashMap::new(); + md.insert("DATA_TYPE".to_string(), type_str.to_string()); + if let Some(pk) = &primary_keys { + md.insert("PRIMARY_KEY".to_string(), pk.value(i).to_string()); + } + if let Some(cm) = &comments { + if !cm.is_null(i) { + md.insert("COMMENT".to_string(), cm.value(i).to_string()); + } + } + fields.push( + Field::new(names.value(i), arrow_type, nullables.value(i) == "Y") + .with_metadata(md), + ); } } Ok(Schema::new(fields)) From 1174d73e34411a78b4e3c9ec85012db918117502 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Fri, 20 Mar 2026 12:04:37 -0400 Subject: [PATCH 26/76] clippy --- rust/adbc-snowflake/src/connection.rs | 10 +++++----- rust/adbc-snowflake/src/database.rs | 24 +++++++++++------------- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/rust/adbc-snowflake/src/connection.rs b/rust/adbc-snowflake/src/connection.rs index ca0285b..dae1bbd 100644 --- a/rust/adbc-snowflake/src/connection.rs +++ b/rust/adbc-snowflake/src/connection.rs @@ -273,7 +273,7 @@ impl adbc_core::Connection for Connection { codes: Option>, ) -> Result> { let need_vendor_version = - codes.as_ref().map_or(true, |s| s.contains(&InfoCode::VendorVersion)); + codes.as_ref().is_none_or(|s| s.contains(&InfoCode::VendorVersion)); let vendor_version = if need_vendor_version { self.query_scalar("SELECT CURRENT_VERSION()")? } else { @@ -495,10 +495,10 @@ impl adbc_core::Connection for Connection { if let Some(pk) = &primary_keys { md.insert("PRIMARY_KEY".to_string(), pk.value(i).to_string()); } - if let Some(cm) = &comments { - if !cm.is_null(i) { - md.insert("COMMENT".to_string(), cm.value(i).to_string()); - } + if let Some(cm) = &comments + && !cm.is_null(i) + { + md.insert("COMMENT".to_string(), cm.value(i).to_string()); } fields.push( Field::new(names.value(i), arrow_type, nullables.value(i) == "Y") diff --git a/rust/adbc-snowflake/src/database.rs b/rust/adbc-snowflake/src/database.rs index db98926..e9e3634 100644 --- a/rust/adbc-snowflake/src/database.rs +++ b/rust/adbc-snowflake/src/database.rs @@ -402,20 +402,18 @@ impl adbc_core::Database for Database { // If neither host nor server_url was provided, derive host from account. if !self.sf_settings.contains_key(param_names::HOST.as_str()) && !self.sf_settings.contains_key(param_names::SERVER_URL.as_str()) - { - if let Some(Setting::String(account)) = + && let Some(Setting::String(account)) = self.sf_settings.get(param_names::ACCOUNT.as_str()) - { - let host = format!("{}.snowflakecomputing.com", account); - self.inner - .runtime - .block_on(self.inner.sf.connection_set_option( - conn_handle, - param_names::HOST.into(), - Setting::String(host), - )) - .map_err(crate::error::api_error_to_adbc_error)?; - } + { + let host = format!("{}.snowflakecomputing.com", account); + self.inner + .runtime + .block_on(self.inner.sf.connection_set_option( + conn_handle, + param_names::HOST.into(), + Setting::String(host), + )) + .map_err(crate::error::api_error_to_adbc_error)?; } // Authenticate From b3fd2b367ef96617a2f475d2940cb42bfcf7fd32 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Fri, 20 Mar 2026 12:04:58 -0400 Subject: [PATCH 27/76] cargo fmt --- rust/adbc-snowflake/src/connection.rs | 44 ++++++++++++---------- rust/adbc-snowflake/src/database.rs | 8 ++-- rust/adbc-snowflake/tests/integration.rs | 47 ++++++++++++++++-------- 3 files changed, 60 insertions(+), 39 deletions(-) diff --git a/rust/adbc-snowflake/src/connection.rs b/rust/adbc-snowflake/src/connection.rs index dae1bbd..5f744de 100644 --- a/rust/adbc-snowflake/src/connection.rs +++ b/rust/adbc-snowflake/src/connection.rs @@ -126,8 +126,8 @@ impl Connection { let _ = self.inner.sf.statement_release(stmt_handle); let exec_result = result.map_err(crate::error::api_error_to_adbc_error)?; - let raw = Box::into_raw(exec_result.stream) - as *mut arrow_array::ffi_stream::FFI_ArrowArrayStream; + let raw = + Box::into_raw(exec_result.stream) as *mut arrow_array::ffi_stream::FFI_ArrowArrayStream; let mut reader = unsafe { arrow_array::ffi_stream::ArrowArrayStreamReader::from_raw(raw) } .map_err(|e| Error::with_message_and_status(e.to_string(), Status::IO))?; @@ -272,8 +272,9 @@ impl adbc_core::Connection for Connection { &self, codes: Option>, ) -> Result> { - let need_vendor_version = - codes.as_ref().is_none_or(|s| s.contains(&InfoCode::VendorVersion)); + let need_vendor_version = codes + .as_ref() + .is_none_or(|s| s.contains(&InfoCode::VendorVersion)); let vendor_version = if need_vendor_version { self.query_scalar("SELECT CURRENT_VERSION()")? } else { @@ -350,7 +351,11 @@ impl adbc_core::Connection for Connection { "entries", vec![ Field::new("key", DataType::Int32, false), - Field::new_list("value", Field::new_list_field(DataType::Int32, true), true), + Field::new_list( + "value", + Field::new_list_field(DataType::Int32, true), + true, + ), ], false, )), @@ -380,7 +385,11 @@ impl adbc_core::Connection for Connection { "int32_to_int32_list_map", "entries", Field::new("key", DataType::Int32, false), - Field::new_list("value", Field::new_list_field(DataType::Int32, true), true), + Field::new_list( + "value", + Field::new_list_field(DataType::Int32, true), + true, + ), false, true, ), @@ -479,10 +488,9 @@ impl adbc_core::Connection for Connection { let nullables = batch.column(3).as_string::(); // primary_key is column 5; comment is column 9 — present only when // the result has enough columns (older Snowflake editions may omit them). - let primary_keys = (batch.num_columns() > 5) - .then(|| batch.column(5).as_string::()); - let comments = (batch.num_columns() > 9) - .then(|| batch.column(9).as_string::()); + let primary_keys = + (batch.num_columns() > 5).then(|| batch.column(5).as_string::()); + let comments = (batch.num_columns() > 9).then(|| batch.column(9).as_string::()); for i in 0..batch.num_rows() { let type_str = types.value(i); let arrow_type = snowflake_type_to_arrow( @@ -605,9 +613,7 @@ fn snowflake_type_to_arrow( } } "TIMESTAMP" | "TIMESTAMP_NTZ" | "DATETIME" => DataType::Timestamp(ts_unit, None), - "TIMESTAMP_LTZ" | "TIMESTAMP_TZ" => { - DataType::Timestamp(ts_unit, Some("UTC".into())) - } + "TIMESTAMP_LTZ" | "TIMESTAMP_TZ" => DataType::Timestamp(ts_unit, Some("UTC".into())), _ => DataType::Utf8, } } @@ -662,7 +668,11 @@ mod tests { DataType::Utf8 ); assert_eq!( - snowflake_type_to_arrow("VARCHAR(16777216)", true, arrow_schema::TimeUnit::Nanosecond), + snowflake_type_to_arrow( + "VARCHAR(16777216)", + true, + arrow_schema::TimeUnit::Nanosecond + ), DataType::Utf8 ); } @@ -678,11 +688,7 @@ mod tests { #[test] fn snowflake_type_timestamp_ntz_nanosecond() { assert_eq!( - snowflake_type_to_arrow( - "TIMESTAMP_NTZ(9)", - true, - arrow_schema::TimeUnit::Nanosecond - ), + snowflake_type_to_arrow("TIMESTAMP_NTZ(9)", true, arrow_schema::TimeUnit::Nanosecond), DataType::Timestamp(arrow_schema::TimeUnit::Nanosecond, None) ); } diff --git a/rust/adbc-snowflake/src/database.rs b/rust/adbc-snowflake/src/database.rs index e9e3634..d137c04 100644 --- a/rust/adbc-snowflake/src/database.rs +++ b/rust/adbc-snowflake/src/database.rs @@ -308,9 +308,7 @@ impl Database { "port" => "adbc.snowflake.sql.uri.port", "protocol" => "adbc.snowflake.sql.uri.protocol", "authenticator" => "adbc.snowflake.sql.auth_type", - "private_key_file" => { - "adbc.snowflake.sql.client_option.jwt_private_key" - } + "private_key_file" => "adbc.snowflake.sql.client_option.jwt_private_key", "private_key" => { "adbc.snowflake.sql.client_option.jwt_private_key_pkcs8_value" } @@ -401,7 +399,9 @@ impl adbc_core::Database for Database { // If neither host nor server_url was provided, derive host from account. if !self.sf_settings.contains_key(param_names::HOST.as_str()) - && !self.sf_settings.contains_key(param_names::SERVER_URL.as_str()) + && !self + .sf_settings + .contains_key(param_names::SERVER_URL.as_str()) && let Some(Setting::String(account)) = self.sf_settings.get(param_names::ACCOUNT.as_str()) { diff --git a/rust/adbc-snowflake/tests/integration.rs b/rust/adbc-snowflake/tests/integration.rs index 872cabc..65d8e06 100644 --- a/rust/adbc-snowflake/tests/integration.rs +++ b/rust/adbc-snowflake/tests/integration.rs @@ -77,9 +77,18 @@ fn test_private_key_simple_query() { assert_eq!(batch.num_rows(), 1); assert_eq!(batch.num_columns(), 3); - assert!(!batch.column(0).as_string::().value(0).is_empty(), "CURRENT_USER() is empty"); - assert!(!batch.column(1).as_string::().value(0).is_empty(), "CURRENT_WAREHOUSE() is empty"); - assert!(!batch.column(2).as_string::().value(0).is_empty(), "CURRENT_ROLE() is empty"); + assert!( + !batch.column(0).as_string::().value(0).is_empty(), + "CURRENT_USER() is empty" + ); + assert!( + !batch.column(1).as_string::().value(0).is_empty(), + "CURRENT_WAREHOUSE() is empty" + ); + assert!( + !batch.column(2).as_string::().value(0).is_empty(), + "CURRENT_ROLE() is empty" + ); } #[test] @@ -143,13 +152,24 @@ fn test_get_info_vendor_version() { // VendorVersion is a string value — type_id 0 in the union use arrow_array::cast::AsArray; - let type_ids = batch.column(1).as_any().downcast_ref::().unwrap(); - assert_eq!(type_ids.type_id(0), 0, "VendorVersion should be a string union arm"); + let type_ids = batch + .column(1) + .as_any() + .downcast_ref::() + .unwrap(); + assert_eq!( + type_ids.type_id(0), + 0, + "VendorVersion should be a string union arm" + ); let version_str = type_ids.value(0); let v = version_str.as_string::().value(0); assert!(!v.is_empty(), "VendorVersion should not be empty"); // Snowflake versions look like "8.x.x" — just check it contains a dot - assert!(v.contains('.'), "VendorVersion should look like a version string, got: {v}"); + assert!( + v.contains('.'), + "VendorVersion should look like a version string, got: {v}" + ); } #[test] @@ -274,9 +294,7 @@ fn test_precision_options_defaults_and_round_trip() { // Round-trip: microsecond timestamps db.set_option( - OptionDatabase::Other( - "adbc.snowflake.sql.client_option.max_timestamp_precision".into(), - ), + OptionDatabase::Other("adbc.snowflake.sql.client_option.max_timestamp_precision".into()), OptionValue::String("microseconds".into()), ) .unwrap(); @@ -290,9 +308,7 @@ fn test_precision_options_defaults_and_round_trip() { // Round-trip: nanoseconds_error_on_overflow db.set_option( - OptionDatabase::Other( - "adbc.snowflake.sql.client_option.max_timestamp_precision".into(), - ), + OptionDatabase::Other("adbc.snowflake.sql.client_option.max_timestamp_precision".into()), OptionValue::String("nanoseconds_error_on_overflow".into()), ) .unwrap(); @@ -348,9 +364,7 @@ fn test_high_precision_get_table_schema() { let Some(mut db_lp) = make_db() else { return }; db_lp .set_option( - OptionDatabase::Other( - "adbc.snowflake.sql.client_option.use_high_precision".into(), - ), + OptionDatabase::Other("adbc.snowflake.sql.client_option.use_high_precision".into()), OptionValue::String("disabled".into()), ) .unwrap(); @@ -400,7 +414,8 @@ fn test_timestamp_precision_get_table_schema() { (NTZ_COL TIMESTAMP_NTZ, TZ_COL TIMESTAMP_TZ)", ) .unwrap(); - stmt.execute_update().expect("create ts precision test table"); + stmt.execute_update() + .expect("create ts precision test table"); } // Snowflake folds unquoted identifiers to uppercase. From b15e13a76f5f2dc3489d37ca0140689745be222e Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Fri, 20 Mar 2026 13:16:39 -0400 Subject: [PATCH 28/76] add pixi stuff for rust impl --- .github/workflows/go_test.yaml | 1 - .github/workflows/rust_test.yaml | 601 ++++++++ rust/adbc-snowflake/.gitattributes | 2 + rust/adbc-snowflake/.gitignore | 3 + rust/adbc-snowflake/manifest.toml | 21 + rust/adbc-snowflake/pixi.lock | 1707 ++++++++++++++++++++++ rust/adbc-snowflake/pixi.toml | 38 + rust/adbc-snowflake/src/database.rs | 228 +++ rust/adbc-snowflake/tests/integration.rs | 124 ++ 9 files changed, 2724 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/rust_test.yaml create mode 100644 rust/adbc-snowflake/.gitattributes create mode 100644 rust/adbc-snowflake/.gitignore create mode 100644 rust/adbc-snowflake/manifest.toml create mode 100644 rust/adbc-snowflake/pixi.lock create mode 100644 rust/adbc-snowflake/pixi.toml diff --git a/.github/workflows/go_test.yaml b/.github/workflows/go_test.yaml index e06f3f9..7d146c6 100644 --- a/.github/workflows/go_test.yaml +++ b/.github/workflows/go_test.yaml @@ -121,7 +121,6 @@ jobs: pixi-version: v0.63.2 run-install: false - - name: Build working-directory: go run: | diff --git a/.github/workflows/rust_test.yaml b/.github/workflows/rust_test.yaml new file mode 100644 index 0000000..a39493d --- /dev/null +++ b/.github/workflows/rust_test.yaml @@ -0,0 +1,601 @@ +# Copyright (c) 2025-2026 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Rust Test + +on: + pull_request: + branches: + - main + paths: + - "rust/**" + - .github/workflows/rust_test.yaml + push: + branches: + - main + paths: + - "rust/**" + - .github/workflows/rust_test.yaml + + workflow_call: + inputs: + repository: + description: "The repository to checkout (in owner/repo short format)" + required: true + type: string + ref: + description: "The ref to checkout" + required: true + type: string + secrets: + SNOWFLAKE_DATABASE: + required: true + SNOWFLAKE_SCHEMA: + required: true + SNOWFLAKE_URI: + required: true + SNOWFLAKE_SCHEMA_SECONDARY: + required: true + SNOWFLAKE_DATABASE_SECONDARY: + required: true + SNOWFLAKE_DATABASE_SECONDARY_SCHEMA: + required: true + +concurrency: + group: ${{ github.repository }}-${{ github.ref }}-Snowflake-Rust-CI + cancel-in-progress: true + +defaults: + run: + shell: bash + +jobs: + test: + name: "Test/${{ matrix.platform }}_${{ matrix.arch }}" + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: true + matrix: + include: + - { platform: linux, arch: amd64, runner: ubuntu-latest } + - { platform: macos, arch: arm64, runner: macos-latest } + - { platform: windows, arch: amd64, runner: windows-latest } + environment: Snowflake CI + env: + CARGO_INCREMENTAL: 0 + permissions: + contents: read + steps: + - name: free up disk space + if: runner.os != 'Windows' + run: | + # Preinstalled tools use a lot of disk space, free up some space + # https://github.com/actions/runner-images/issues/2840 + sudo rm -rf "$AGENT_TOOLSDIRECTORY" + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + if: github.event_name != 'workflow_dispatch' + with: + fetch-depth: 0 + persist-credentials: false + submodules: 'recursive' + + - name: "checkout remote" + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + if: github.event_name == 'workflow_dispatch' + with: + repository: ${{ inputs.repository }} + ref: ${{ inputs.ref }} + fetch-depth: 0 + persist-credentials: false + submodules: 'recursive' + + - uses: dtolnay/rust-toolchain@efa25f7f19611383d5b0ccf2d1c8914531636bf9 + with: + toolchain: stable + + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 + with: + workspaces: rust/adbc-snowflake + + - uses: prefix-dev/setup-pixi@a0af7a228712d6121d37aba47adf55c1332c9c2e # v0.9.4 + with: + pixi-version: v0.63.2 + run-install: false + + - name: Build + working-directory: rust/adbc-snowflake + run: | + if [[ -f ci/scripts/pre-build.sh ]]; then + echo "Loading pre-build" + ./ci/scripts/pre-build.sh test ${{ matrix.platform }} ${{ matrix.arch }} + fi + + set -a + if [[ -f .env.build ]]; then + echo "Loading .env.build" + source .env.build + fi + set +a + cargo build + + - name: Start Test Dependencies + # Can't use Docker on macOS AArch64 runners, and Windows containers + # work but often the container doesn't support Windows + if: runner.os == 'Linux' + working-directory: rust/adbc-snowflake + run: | + if [[ -f compose.yaml ]]; then + if ! docker compose up --detach --wait test-service; then + echo "Service failed to start" + echo "Logs:" + docker compose logs test-service + exit 1 + fi + fi + + - name: Test + working-directory: rust/adbc-snowflake + env: + SNOWFLAKE_DATABASE: ${{ secrets.SNOWFLAKE_DATABASE }} + SNOWFLAKE_SCHEMA: ${{ secrets.SNOWFLAKE_SCHEMA }} + SNOWFLAKE_URI: ${{ secrets.SNOWFLAKE_URI }} + SNOWFLAKE_SCHEMA_SECONDARY: ${{ secrets.SNOWFLAKE_SCHEMA_SECONDARY }} + SNOWFLAKE_DATABASE_SECONDARY: ${{ secrets.SNOWFLAKE_DATABASE_SECONDARY }} + SNOWFLAKE_DATABASE_SECONDARY_SCHEMA: ${{ secrets.SNOWFLAKE_DATABASE_SECONDARY_SCHEMA }} + run: | + set -a + if [[ -f .env ]]; then + source .env + fi + if [[ -f .env.${{ matrix.platform }} ]]; then + source .env.${{ matrix.platform }} + fi + if [[ -f .env.ci ]]; then + source .env.ci + fi + set +a + + if [[ -f ci/scripts/pre-test.sh ]]; then + echo "Loading pre-test" + ./ci/scripts/pre-test.sh ${{ matrix.platform }} ${{ matrix.arch }} + fi + + cargo test + + if [[ -f ci/scripts/post-test.sh ]]; then + ./ci/scripts/post-test.sh + fi + + - name: Lint + if: runner.os == 'Linux' + working-directory: rust/adbc-snowflake + run: | + cargo fmt --check + cargo clippy -- -D warnings + + validate: + name: "Validate/${{ matrix.platform }}_${{ matrix.arch }}" + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: true + matrix: + include: + # I think we only need to test one platform, but we can change that later + - { platform: linux, arch: amd64, runner: ubuntu-latest } + environment: Snowflake CI + env: + CARGO_INCREMENTAL: 0 + permissions: + contents: read + steps: + - name: free up disk space + if: runner.os != 'Windows' + run: | + # Preinstalled tools use a lot of disk space, free up some space + # https://github.com/actions/runner-images/issues/2840 + sudo rm -rf "$AGENT_TOOLSDIRECTORY" + + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + if: github.event_name != 'workflow_dispatch' + with: + fetch-depth: 0 + persist-credentials: false + submodules: 'recursive' + + - name: "checkout remote" + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + if: github.event_name == 'workflow_dispatch' + with: + repository: ${{ inputs.repository }} + ref: ${{ inputs.ref }} + fetch-depth: 0 + persist-credentials: false + submodules: 'recursive' + + - uses: dtolnay/rust-toolchain@efa25f7f19611383d5b0ccf2d1c8914531636bf9 + with: + toolchain: stable + + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 + with: + workspaces: rust/adbc-snowflake + + - uses: prefix-dev/setup-pixi@a0af7a228712d6121d37aba47adf55c1332c9c2e # v0.9.4 + with: + pixi-version: v0.63.2 + run-install: false + + - name: Log in to ghcr.io + if: runner.os == 'Linux' + env: + GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo "$GHCR_TOKEN" | docker login ghcr.io -u "$GITHUB_ACTOR" --password-stdin + + - name: Build Library + working-directory: rust/adbc-snowflake + run: | + if [[ -f ci/scripts/pre-build.sh ]]; then + ./ci/scripts/pre-build.sh test ${{ matrix.platform }} ${{ matrix.arch }} + fi + set -a + if [[ -f .env.build ]]; then + echo "Loading .env.build" + source .env.build + fi + if [[ -f .env.test ]]; then + source .env.test + fi + set +a + pixi run adbc-make build DEBUG=true VERBOSE=true DRIVER=snowflake IMPL_LANG=rust BUILD_TAGS=minicore_disabled + + - name: Start Test Dependencies + # Can't use Docker on macOS AArch64 runners, and windows containers + # work but often the container doesn't support Windows + if: runner.os == 'Linux' + working-directory: rust/adbc-snowflake + run: | + if [[ -f compose.yaml ]]; then + if ! docker compose up --detach --wait test-service; then + echo "Service failed to start" + echo "Logs:" + docker compose logs test-service + exit 1 + fi + fi + + - name: Validate + if: runner.os == 'Linux' + env: + SNOWFLAKE_DATABASE: ${{ secrets.SNOWFLAKE_DATABASE }} + SNOWFLAKE_SCHEMA: ${{ secrets.SNOWFLAKE_SCHEMA }} + SNOWFLAKE_URI: ${{ secrets.SNOWFLAKE_URI }} + SNOWFLAKE_SCHEMA_SECONDARY: ${{ secrets.SNOWFLAKE_SCHEMA_SECONDARY }} + SNOWFLAKE_DATABASE_SECONDARY: ${{ secrets.SNOWFLAKE_DATABASE_SECONDARY }} + SNOWFLAKE_DATABASE_SECONDARY_SCHEMA: ${{ secrets.SNOWFLAKE_DATABASE_SECONDARY_SCHEMA }} + working-directory: rust/adbc-snowflake + run: | + set -a + if [[ -f .env ]]; then + source .env + fi + if [[ -f .env.${{ matrix.platform }} ]]; then + source .env.${{ matrix.platform }} + fi + if [[ -f .env.ci ]]; then + source .env.ci + fi + set +a + + if [[ -f ci/scripts/pre-test.sh ]]; then + echo "Loading pre-test" + ./ci/scripts/pre-test.sh ${{ matrix.platform }} ${{ matrix.arch }} + fi + + docker ps + pixi run validate + + if [[ -f ci/scripts/post-test.sh ]]; then + ./ci/scripts/post-test.sh + fi + + - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: validation-report + path: "rust/adbc-snowflake/validation-report.xml" + retention-days: 7 + + - name: Generate docs + working-directory: rust/adbc-snowflake + run: | + pixi run gendocs --output generated + + - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: docs + path: "rust/adbc-snowflake/generated/snowflake.md" + retention-days: 2 + + build: + name: "Build adbc-snowflake/${{ matrix.platform }}_${{ matrix.arch }}" + needs: test + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: true + matrix: + include: + - { platform: linux, arch: amd64, runner: ubuntu-latest } + - { platform: linux, arch: arm64, runner: ubuntu-24.04-arm } + - { platform: macos, arch: arm64, runner: macos-latest } + - { platform: windows, arch: amd64, runner: windows-latest } + permissions: + contents: read + packages: read + env: + CARGO_INCREMENTAL: 0 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + if: github.event_name != 'workflow_dispatch' + with: + fetch-depth: 0 + persist-credentials: false + submodules: 'recursive' + + - name: "checkout remote" + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + if: github.event_name == 'workflow_dispatch' + with: + repository: ${{ inputs.repository }} + ref: ${{ inputs.ref }} + fetch-depth: 0 + persist-credentials: false + submodules: 'recursive' + + - uses: dtolnay/rust-toolchain@efa25f7f19611383d5b0ccf2d1c8914531636bf9 + with: + toolchain: stable + + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 + with: + workspaces: rust/adbc-snowflake + + - uses: prefix-dev/setup-pixi@a0af7a228712d6121d37aba47adf55c1332c9c2e # v0.9.4 + with: + pixi-version: v0.63.2 + run-install: false + + - name: Install dev tools + working-directory: rust/adbc-snowflake + run: | + pixi install + + - name: Log in to ghcr.io + if: runner.os == 'Linux' + env: + GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo "$GHCR_TOKEN" | docker login ghcr.io -u ${{ github.actor }} --password-stdin + + - name: Build Library + working-directory: rust/adbc-snowflake + run: | + if [[ -f ci/scripts/pre-build.sh ]]; then + ./ci/scripts/pre-build.sh release ${{ matrix.platform }} ${{ matrix.arch }} + fi + set -a + if [[ -f .env.build ]]; then + echo "Loading .env.build" + source .env.build + fi + if [[ -f .env.release ]]; then + source .env.release + fi + set +a + pixi run adbc-make check CI=true VERBOSE=true DRIVER=snowflake IMPL_LANG=rust BUILD_TAGS=minicore_disabled + + - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: drivers-${{ matrix.platform }}-${{ matrix.arch }} + path: rust/adbc-snowflake/target/release/libadbc_snowflake.* + retention-days: 2 + + package: + name: "Generate Packages" + runs-on: ubuntu-latest + needs: build + permissions: + contents: read + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + if: github.event_name != 'workflow_dispatch' + with: + fetch-depth: 0 + persist-credentials: false + submodules: 'recursive' + + - name: "checkout remote" + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + if: github.event_name == 'workflow_dispatch' + with: + repository: ${{ inputs.repository }} + ref: ${{ inputs.ref }} + fetch-depth: 0 + persist-credentials: false + submodules: 'recursive' + + - uses: dtolnay/rust-toolchain@efa25f7f19611383d5b0ccf2d1c8914531636bf9 + with: + toolchain: stable + + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 + with: + workspaces: rust/adbc-snowflake + + - uses: prefix-dev/setup-pixi@a0af7a228712d6121d37aba47adf55c1332c9c2e # v0.9.4 + with: + pixi-version: v0.63.2 + run-install: false + + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: "drivers-*" + path: "~/drivers" + + - name: Generate packages + working-directory: rust/adbc-snowflake + run: | + pixi install + + pixi run adbc-gen-package \ + --name snowflake \ + --root $(pwd) \ + --manifest-template $(pwd)/manifest.toml \ + ${{ (inputs.release && '--release') || '' }}\ + -o ~/packages \ + ~/drivers/drivers-*-*/ + + ls ~/packages + + - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: all-packages + path: ~/packages + retention-days: 7 + + test-packages: + name: "Test Packages/${{ matrix.platform }}_${{ matrix.arch }}" + runs-on: ${{ matrix.runner }} + needs: + - package + strategy: + fail-fast: false + matrix: + include: + - { platform: linux, arch: amd64, runner: ubuntu-latest } + - { platform: macos, arch: arm64, runner: macos-latest } + - { platform: windows, arch: amd64, runner: windows-latest } + steps: + # for now, install dbc from main + - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 + with: + go-version: 'stable' + - uses: prefix-dev/setup-pixi@a0af7a228712d6121d37aba47adf55c1332c9c2e # v0.9.4 + with: + pixi-version: v0.63.2 + run-install: false + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: "all-packages" + path: "~/packages" + - name: install package + # dbc uses the filename as the driver name + run: | + echo "Installing dbc" + git clone --depth 1 https://github.com/columnar-tech/dbc + cd dbc + go build ./cmd/dbc + echo "Installed dbc" + ls -laR ~/packages + driver_pkg=$(find ~/packages -name '*_${{ matrix.platform }}_${{ matrix.arch }}_*.tar.gz') + echo "Installing ${driver_pkg}" + ./dbc install --no-verify "${driver_pkg}" + echo "Installed ${driver_pkg}" + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + if: github.event_name != 'workflow_dispatch' + with: + fetch-depth: 1 + persist-credentials: false + submodules: 'recursive' + - name: "checkout remote" + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + if: github.event_name == 'workflow_dispatch' + with: + repository: ${{ inputs.repository }} + ref: ${{ inputs.ref }} + fetch-depth: 1 + persist-credentials: false + submodules: 'recursive' + - name: load package + working-directory: rust/adbc-snowflake + run: | + if [[ -f ci/scripts/pre-build.sh ]]; then + echo "Loading pre-build" + ./ci/scripts/pre-build.sh test ${{ matrix.platform }} ${{ matrix.arch }} + fi + set -a + if [[ -f .env.build ]]; then + echo "Loading .env.build" + source .env.build + fi + if [[ -f .env.ci ]]; then + source .env.ci + fi + set +a + pixi exec -s adbc-driver-manager -s pyarrow -s pytest python -m pytest -vs ci/test_package.py + + release: + name: "Release (Dry Run)" + runs-on: ubuntu-latest + needs: + - package + - test-packages + - validate + permissions: + contents: read + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + if: github.event_name != 'workflow_dispatch' + with: + fetch-depth: 0 + persist-credentials: false + submodules: 'recursive' + + - name: "checkout remote" + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + if: github.event_name == 'workflow_dispatch' + with: + repository: ${{ inputs.repository }} + ref: ${{ inputs.ref }} + fetch-depth: 0 + persist-credentials: false + submodules: 'recursive' + + - uses: prefix-dev/setup-pixi@a0af7a228712d6121d37aba47adf55c1332c9c2e # v0.9.4 + with: + pixi-version: v0.63.2 + run-install: false + + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: "all-packages" + path: "~/packages" + + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: "docs" + path: "~/packages" + + - name: Release (dry-run) + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + working-directory: rust/adbc-snowflake + run: | + git tag go/v1000.0.0 + tag=go/v1000.0.0 + + pixi run release --dry-run $(pwd) $tag + echo gh release upload $tag $(find ~/packages -name '*.tar.gz') $(find ~/packages -name 'manifest.yaml') $(find ~/packages -name '*.md') diff --git a/rust/adbc-snowflake/.gitattributes b/rust/adbc-snowflake/.gitattributes new file mode 100644 index 0000000..887a2c1 --- /dev/null +++ b/rust/adbc-snowflake/.gitattributes @@ -0,0 +1,2 @@ +# SCM syntax highlighting & preventing 3-way merges +pixi.lock merge=binary linguist-language=YAML linguist-generated=true diff --git a/rust/adbc-snowflake/.gitignore b/rust/adbc-snowflake/.gitignore new file mode 100644 index 0000000..ae849e6 --- /dev/null +++ b/rust/adbc-snowflake/.gitignore @@ -0,0 +1,3 @@ +# pixi environments +.pixi/* +!.pixi/config.toml diff --git a/rust/adbc-snowflake/manifest.toml b/rust/adbc-snowflake/manifest.toml new file mode 100644 index 0000000..46d65f2 --- /dev/null +++ b/rust/adbc-snowflake/manifest.toml @@ -0,0 +1,21 @@ +# Copyright (c) 2025 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name = "ADBC Driver Foundry Driver for Snowflake (rust)" +description = "An ADBC driver for Snowflake developed by the ADBC Driver Foundry" +publisher = "ADBC Drivers Contributors" +license = "Apache-2.0" + +[ADBC] +version = "v1.1.0" diff --git a/rust/adbc-snowflake/pixi.lock b/rust/adbc-snowflake/pixi.lock new file mode 100644 index 0000000..c279f49 --- /dev/null +++ b/rust/adbc-snowflake/pixi.lock @@ -0,0 +1,1707 @@ +version: 6 +environments: + default: + channels: + - url: https://conda.anaconda.org/conda-forge/ + indexes: + - https://pypi.org/simple + packages: + linux-64: + - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.2.25-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.3-h33c6efd_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_101.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.4-hecca717_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.2-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb03c661_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.52.0-hf4e2dac_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.3-h5347b49_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.1-h35e630c_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.0.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-lazy-fixtures-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.12-hc97d973_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda + - pypi: https://files.pythonhosted.org/packages/ac/7d/3e131221995aef7edfd4dd0b09f14b7e51772d28eb362a0e6c3b8301a22a/adbc_driver_manager-1.10.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/2b/58/a199d245894b12db0b957d627516c78e055adc3a0d978bc7f65ddaf7c399/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: git+https://github.com/adbc-drivers/dev#ffdbb1a1237b89cafb15a7420667f54838986681 + - pypi: https://files.pythonhosted.org/packages/9c/38/3d6dcbf8379cb86d71a2325210ca3469a33767d8254b74d8c343db26ce87/doit-0.37.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/54/13/b58d718415cde993823a54952ea511d2612302f1d2bc220549d0cef752a4/duckdb-1.5.0-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/b3/93/10a48b5e238de6d562a411af6467e71e7aedbc9b87f8d3a35f1560ae30fb/pyarrow-23.0.1-cp313-cp313-manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/d5/80/1a87f6e043e04cfa125380a73ef9f87a8c58292b7d4a6ed2e6203b4cd534/pygit2-1.19.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/fe/b6045c782f1fd1ae317d2a6ca1884857ce5c20f59befe6ab25a8603c43a7/ruamel_yaml-0.18.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ff/5d/e4f84c9c448613e12bd62e90b23aa127ea4c46b697f3d760acc32cb94f25/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + - pypi: https://files.pythonhosted.org/packages/c6/13/b57ab75b0f60b5ee8cb8924bc01a5c419ed3221e00f8f11f8c059a707eb7/sqlglot-30.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl + - pypi: git+https://github.com/adbc-drivers/validation#80cd7910a2c7495301d617363a006c435cb6d044 + - pypi: https://files.pythonhosted.org/packages/56/5c/8d6dc529595b5387f5727cd6c2c5b8615851d95fec5c599a61ef239cc1b3/whenever-0.9.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + linux-aarch64: + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-20_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/bzip2-1.0.8-h4777abc_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.2.25-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/icu-78.3-hcab7f73_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ld_impl_linux-aarch64-2.45.1-default_h1979696_101.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libexpat-2.7.4-hfae3067_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libffi-3.5.2-h376a255_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-15.2.0-h8acb6b2_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgomp-15.2.0-h8acb6b2_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/liblzma-5.8.2-he30d5cf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libmpdec-4.0.0-he30d5cf_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsqlite-3.52.0-h10b116e_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-15.2.0-hef695bb_18.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libuuid-2.41.3-h1022ec0_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libzlib-1.3.1-h86ecc28_2.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ncurses-6.5-ha32ae93_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/openssl-3.6.1-h546c87b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.0.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-lazy-fixtures-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/python-3.13.12-h4c0d347_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/readline-8.3-hb682ff5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/tk-8.6.13-noxft_h0dc03b3_103.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zstd-1.5.7-h85ac4a6_6.conda + - pypi: https://files.pythonhosted.org/packages/52/7b/2c076500e60cac3c2761eeecc82afed42af22d3a65cf3cd8d8034ffd75ad/adbc_driver_manager-1.10.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/47/7b/20e809b89c69d37be748d98e84dce6820bf663cf19cf6b942c951a3e8f41/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: git+https://github.com/adbc-drivers/dev#ffdbb1a1237b89cafb15a7420667f54838986681 + - pypi: https://files.pythonhosted.org/packages/9c/38/3d6dcbf8379cb86d71a2325210ca3469a33767d8254b74d8c343db26ce87/doit-0.37.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/53/07/1390e69db922423b2e111e32ed342b3e8fad0a31c144db70681ea1ba4d56/duckdb-1.5.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/f9/63/d2747d930882c9d661e9398eefc54f15696547b8983aaaf11d4a2e8b5426/pyarrow-23.0.1-cp313-cp313-manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/a7/02/02f0f56b9b0b044018d9047adf68ba842ebda662ba43ace942ed904f8e9d/pygit2-1.19.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/fe/b6045c782f1fd1ae317d2a6ca1884857ce5c20f59befe6ab25a8603c43a7/ruamel_yaml-0.18.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/ce/bb/6ef5abfa43b48dd55c30d53e997f8f978722f02add61efba31380d73e42e/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + - pypi: https://files.pythonhosted.org/packages/c6/13/b57ab75b0f60b5ee8cb8924bc01a5c419ed3221e00f8f11f8c059a707eb7/sqlglot-30.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl + - pypi: git+https://github.com/adbc-drivers/validation#80cd7910a2c7495301d617363a006c435cb6d044 + - pypi: https://files.pythonhosted.org/packages/b5/dc/090732e6e75f15a6084700d3247db6aa1f885971b637531529c62c4ba1c6/whenever-0.9.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + osx-arm64: + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.2.25-hbd8a1cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-78.3-hef89b57_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.7.4-hf6b4638_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.2-h8088a28_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h84a0fba_1.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.52.0-h1ae2325_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.1-hd24854e_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.0.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-lazy-fixtures-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.13.12-h20e6be0_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda + - conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - pypi: https://files.pythonhosted.org/packages/d8/9c/6f9929b53cd578bef06b8d000e0ab829b982bcf5b22a6c99acfbad2aab34/adbc_driver_manager-1.10.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/1e/1d/4fdabeef4e231153b6ed7567602f3b68265ec4e5b76d6024cf647d43d981/charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl + - pypi: git+https://github.com/adbc-drivers/dev#ffdbb1a1237b89cafb15a7420667f54838986681 + - pypi: https://files.pythonhosted.org/packages/9c/38/3d6dcbf8379cb86d71a2325210ca3469a33767d8254b74d8c343db26ce87/doit-0.37.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/76/fc/c916e928606946209c20fb50898dabf120241fb528a244e2bd8cde1bd9e2/duckdb-1.5.0-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/47/10/2cbe4c6f0fb83d2de37249567373d64327a5e4d8db72f486db42875b08f6/pyarrow-23.0.1-cp313-cp313-macosx_12_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/e2/1f/f67ec7f78a34ed14dbd3acf05ed23c4c8c2336ba6f3ca78d6b9962878435/pygit2-1.19.1-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/fe/b6045c782f1fd1ae317d2a6ca1884857ce5c20f59befe6ab25a8603c43a7/ruamel_yaml-0.18.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d6/03/a1baa5b94f71383913f21b96172fb3a2eb5576a4637729adbf7cd9f797f8/ruamel_yaml_clib-0.2.15-cp313-cp313-macosx_11_0_arm64.whl + - pypi: https://files.pythonhosted.org/packages/c6/13/b57ab75b0f60b5ee8cb8924bc01a5c419ed3221e00f8f11f8c059a707eb7/sqlglot-30.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl + - pypi: git+https://github.com/adbc-drivers/validation#80cd7910a2c7495301d617363a006c435cb6d044 + - pypi: https://files.pythonhosted.org/packages/ea/81/d59f0e226ef542fc4bc86567d7b9e2bf9016c353b1f83661ee3913a140a7/whenever-0.9.5-cp313-cp313-macosx_11_0_arm64.whl + win-64: + - conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_9.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.2.25-h4c7d964_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.7.4-hac47afa_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h3d046cb_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.2-hfd05255_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libmpdec-4.0.0-hfd05255_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.52.0-hf5d6505_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.1-h2466b09_2.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.1-hf411b9b_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.0.2-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/pytest-lazy-fixtures-1.4.0-pyhd8ed1ab_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/python-3.13.12-h09917c8_100_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_34.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_34.conda + - pypi: https://files.pythonhosted.org/packages/97/c2/2ed6c856dd56bbc0a45aaab67f6b1f0a846296f20d5ad625a3c5e7084e4f/adbc_driver_manager-1.10.0-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/a1/5c/724b6b363603e419829f561c854b87ed7c7e31231a7908708ac086cdf3e2/charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl + - pypi: git+https://github.com/adbc-drivers/dev#ffdbb1a1237b89cafb15a7420667f54838986681 + - pypi: https://files.pythonhosted.org/packages/9c/38/3d6dcbf8379cb86d71a2325210ca3469a33767d8254b74d8c343db26ce87/doit-0.37.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/e0/96/4460429651e371eb5ff745a4790e7fa0509c7a58c71fc4f0f893404c9646/duckdb-1.5.0-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/d5/09/a532297c9591a727d67760e2e756b83905dd89adb365a7f6e9c72578bcc1/pyarrow-23.0.1-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/6d/01/98f74ecbe92f042d27e4de3cd7f093422d523cc67fdc74e6a65dbe4efbb8/pygit2-1.19.1-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/af/fe/b6045c782f1fd1ae317d2a6ca1884857ce5c20f59befe6ab25a8603c43a7/ruamel_yaml-0.18.17-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/06/c4/c124fbcef0684fcf3c9b72374c2a8c35c94464d8694c50f37eef27f5a145/ruamel_yaml_clib-0.2.15-cp313-cp313-win_amd64.whl + - pypi: https://files.pythonhosted.org/packages/c6/13/b57ab75b0f60b5ee8cb8924bc01a5c419ed3221e00f8f11f8c059a707eb7/sqlglot-30.0.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl + - pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl + - pypi: git+https://github.com/adbc-drivers/validation#80cd7910a2c7495301d617363a006c435cb6d044 + - pypi: https://files.pythonhosted.org/packages/81/b4/17d4bc76ca73c21eb5b7883d10d8bacb7ce7a30a8f36501db2373c63ffb3/whenever-0.9.5-cp313-cp313-win_amd64.whl +packages: +- conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda + build_number: 20 + sha256: 1dd3fffd892081df9726d7eb7e0dea6198962ba775bd88842135a4ddb4deb3c9 + md5: a9f577daf3de00bca7c3c76c0ecbd1de + depends: + - __glibc >=2.17,<3.0.a0 + - libgomp >=7.5.0 + constrains: + - openmp_impl <0.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 28948 + timestamp: 1770939786096 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-20_gnu.conda + build_number: 20 + sha256: a2527b1d81792a0ccd2c05850960df119c2b6d8f5fdec97f2db7d25dc23b1068 + md5: 468fd3bb9e1f671d36c2cbc677e56f1d + depends: + - libgomp >=7.5.0 + constrains: + - openmp_impl <0.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 28926 + timestamp: 1770939656741 +- pypi: https://files.pythonhosted.org/packages/52/7b/2c076500e60cac3c2761eeecc82afed42af22d3a65cf3cd8d8034ffd75ad/adbc_driver_manager-1.10.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl + name: adbc-driver-manager + version: 1.10.0 + sha256: ae24386989dfa055a09c800d13d5278d5d0399aee2548f071f414e6b8af63fc8 + requires_dist: + - typing-extensions + - pandas ; extra == 'dbapi' + - pyarrow>=14.0.1 ; extra == 'dbapi' + - duckdb ; extra == 'test' + - pandas ; extra == 'test' + - polars ; extra == 'test' + - pyarrow>=14.0.1 ; extra == 'test' + - pytest ; extra == 'test' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/97/c2/2ed6c856dd56bbc0a45aaab67f6b1f0a846296f20d5ad625a3c5e7084e4f/adbc_driver_manager-1.10.0-cp313-cp313-win_amd64.whl + name: adbc-driver-manager + version: 1.10.0 + sha256: 564a95617bda8907a0ad0a8bc8fea0c2cf951cea747c0d750a4b1740c828b1ef + requires_dist: + - typing-extensions + - pandas ; extra == 'dbapi' + - pyarrow>=14.0.1 ; extra == 'dbapi' + - duckdb ; extra == 'test' + - pandas ; extra == 'test' + - polars ; extra == 'test' + - pyarrow>=14.0.1 ; extra == 'test' + - pytest ; extra == 'test' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/ac/7d/3e131221995aef7edfd4dd0b09f14b7e51772d28eb362a0e6c3b8301a22a/adbc_driver_manager-1.10.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + name: adbc-driver-manager + version: 1.10.0 + sha256: 97e06da4235dabbd29244c8bd83f769c8995c25abed5d0c2ee2d95ec76d48b8a + requires_dist: + - typing-extensions + - pandas ; extra == 'dbapi' + - pyarrow>=14.0.1 ; extra == 'dbapi' + - duckdb ; extra == 'test' + - pandas ; extra == 'test' + - polars ; extra == 'test' + - pyarrow>=14.0.1 ; extra == 'test' + - pytest ; extra == 'test' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/d8/9c/6f9929b53cd578bef06b8d000e0ab829b982bcf5b22a6c99acfbad2aab34/adbc_driver_manager-1.10.0-cp313-cp313-macosx_11_0_arm64.whl + name: adbc-driver-manager + version: 1.10.0 + sha256: 94cc8b279c90c66f60a499996651340c17eb40d2fd7ad22e1fe73969ab4db1ee + requires_dist: + - typing-extensions + - pandas ; extra == 'dbapi' + - pyarrow>=14.0.1 ; extra == 'dbapi' + - duckdb ; extra == 'test' + - pandas ; extra == 'test' + - polars ; extra == 'test' + - pyarrow>=14.0.1 ; extra == 'test' + - pytest ; extra == 'test' + requires_python: '>=3.10' +- pypi: git+https://github.com/adbc-drivers/dev#ffdbb1a1237b89cafb15a7420667f54838986681 + name: adbc-drivers-dev + version: '0.1' + requires_dist: + - doit + - jinja2 + - packaging + - platformdirs + - pydantic>=2.0 + - pygit2 + - requests + - ruamel-yaml>=0.18.11,<0.19 + - tomlkit>=0.13.2,<0.14 +- pypi: git+https://github.com/adbc-drivers/validation#80cd7910a2c7495301d617363a006c435cb6d044 + name: adbc-drivers-validation + version: '0.1' + requires_dist: + - adbc-driver-manager>=1.6.0,<2 + - bidict>=0.23.1,<1.0.0 + - duckdb>=1.4.1,<2 + - jinja2>=3.1.6,<4 + - pyarrow>=23.0.0,<24 + - pydantic>=2.12.0,<3 + - pytest>=9.0.0,<10 + - sqlglot>=28.5.0 + - whenever>=0.9.3,<0.10 + requires_python: '>=3.13' +- pypi: https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl + name: annotated-types + version: 0.7.0 + sha256: 1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 + requires_dist: + - typing-extensions>=4.0.0 ; python_full_version < '3.9' + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/99/37/e8730c3587a65eb5645d4aba2d27aae48e8003614d6aaf15dda67f702f1f/bidict-0.23.1-py3-none-any.whl + name: bidict + version: 0.23.1 + sha256: 5dae8d4d79b552a71cbabc7deb25dfe8ce710b17ff41711e13010ead2abfc3e5 + requires_python: '>=3.8' +- conda: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-hda65f42_9.conda + sha256: 0b75d45f0bba3e95dc693336fa51f40ea28c980131fec438afb7ce6118ed05f6 + md5: d2ffd7602c02f2b316fd921d39876885 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: bzip2-1.0.6 + license_family: BSD + purls: [] + size: 260182 + timestamp: 1771350215188 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/bzip2-1.0.8-h4777abc_9.conda + sha256: b3495077889dde6bb370938e7db82be545c73e8589696ad0843a32221520ad4c + md5: 840d8fc0d7b3209be93080bc20e07f2d + depends: + - libgcc >=14 + license: bzip2-1.0.6 + license_family: BSD + purls: [] + size: 192412 + timestamp: 1771350241232 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda + sha256: 540fe54be35fac0c17feefbdc3e29725cce05d7367ffedfaaa1bdda234b019df + md5: 620b85a3f45526a8bc4d23fd78fc22f0 + depends: + - __osx >=11.0 + license: bzip2-1.0.6 + license_family: BSD + purls: [] + size: 124834 + timestamp: 1771350416561 +- conda: https://conda.anaconda.org/conda-forge/win-64/bzip2-1.0.8-h0ad9c76_9.conda + sha256: 76dfb71df5e8d1c4eded2dbb5ba15bb8fb2e2b0fe42d94145d5eed4c75c35902 + md5: 4cb8e6b48f67de0b018719cdf1136306 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: bzip2-1.0.6 + license_family: BSD + purls: [] + size: 56115 + timestamp: 1771350256444 +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.2.25-h4c7d964_0.conda + sha256: 37950019c59b99585cee5d30dbc2cc9696ed4e11f5742606a4db1621ed8f94d6 + md5: f001e6e220355b7f87403a4d0e5bf1ca + depends: + - __win + license: ISC + purls: [] + size: 147734 + timestamp: 1772006322223 +- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.2.25-hbd8a1cb_0.conda + sha256: 67cc7101b36421c5913a1687ef1b99f85b5d6868da3abbf6ec1a4181e79782fc + md5: 4492fd26db29495f0ba23f146cd5638d + depends: + - __unix + license: ISC + purls: [] + size: 147413 + timestamp: 1772006283803 +- pypi: https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl + name: certifi + version: 2026.2.25 + sha256: 027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl + name: cffi + version: 2.0.0 + sha256: 19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75 + requires_dist: + - pycparser ; implementation_name != 'PyPy' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl + name: cffi + version: 2.0.0 + sha256: 45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca + requires_dist: + - pycparser ; implementation_name != 'PyPy' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl + name: cffi + version: 2.0.0 + sha256: c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26 + requires_dist: + - pycparser ; implementation_name != 'PyPy' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl + name: cffi + version: 2.0.0 + sha256: d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b + requires_dist: + - pycparser ; implementation_name != 'PyPy' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/1e/1d/4fdabeef4e231153b6ed7567602f3b68265ec4e5b76d6024cf647d43d981/charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl + name: charset-normalizer + version: 3.4.6 + sha256: 11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/2b/58/a199d245894b12db0b957d627516c78e055adc3a0d978bc7f65ddaf7c399/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: charset-normalizer + version: 3.4.6 + sha256: 530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/47/7b/20e809b89c69d37be748d98e84dce6820bf663cf19cf6b942c951a3e8f41/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + name: charset-normalizer + version: 3.4.6 + sha256: 423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843 + requires_python: '>=3.7' +- pypi: https://files.pythonhosted.org/packages/a1/5c/724b6b363603e419829f561c854b87ed7c7e31231a7908708ac086cdf3e2/charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl + name: charset-normalizer + version: 3.4.6 + sha256: 572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389 + requires_python: '>=3.7' +- conda: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_1.conda + sha256: ab29d57dc70786c1269633ba3dff20288b81664d3ff8d21af995742e2bb03287 + md5: 962b9857ee8e7018c22f2776ffa0b2d7 + depends: + - python >=3.9 + license: BSD-3-Clause + license_family: BSD + purls: + - pkg:pypi/colorama?source=hash-mapping + size: 27011 + timestamp: 1733218222191 +- pypi: https://files.pythonhosted.org/packages/9c/38/3d6dcbf8379cb86d71a2325210ca3469a33767d8254b74d8c343db26ce87/doit-0.37.0-py3-none-any.whl + name: doit + version: 0.37.0 + sha256: a9f181566aa90faac515e276f85e6526019554ed7e13c12cf9dc094ffecf3e1b + requires_dist: + - tomli ; python_full_version < '3.11' and extra == 'toml' + - cloudpickle ; platform_python_implementation != 'PyPy' and extra == 'cloudpickle' + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/53/07/1390e69db922423b2e111e32ed342b3e8fad0a31c144db70681ea1ba4d56/duckdb-1.5.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl + name: duckdb + version: 1.5.0 + sha256: 9409ed1184b363ddea239609c5926f5148ee412b8d9e5ffa617718d755d942f6 + requires_dist: + - ipython ; extra == 'all' + - fsspec ; extra == 'all' + - numpy ; extra == 'all' + - pandas ; extra == 'all' + - pyarrow ; extra == 'all' + - adbc-driver-manager ; extra == 'all' + requires_python: '>=3.10.0' +- pypi: https://files.pythonhosted.org/packages/54/13/b58d718415cde993823a54952ea511d2612302f1d2bc220549d0cef752a4/duckdb-1.5.0-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl + name: duckdb + version: 1.5.0 + sha256: 1df8c4f9c853a45f3ec1e79ed7fe1957a203e5ec893bbbb853e727eb93e0090f + requires_dist: + - ipython ; extra == 'all' + - fsspec ; extra == 'all' + - numpy ; extra == 'all' + - pandas ; extra == 'all' + - pyarrow ; extra == 'all' + - adbc-driver-manager ; extra == 'all' + requires_python: '>=3.10.0' +- pypi: https://files.pythonhosted.org/packages/76/fc/c916e928606946209c20fb50898dabf120241fb528a244e2bd8cde1bd9e2/duckdb-1.5.0-cp313-cp313-macosx_11_0_arm64.whl + name: duckdb + version: 1.5.0 + sha256: 0ee4dabe03ed810d64d93927e0fd18cd137060b81ee75dcaeaaff32cbc816656 + requires_dist: + - ipython ; extra == 'all' + - fsspec ; extra == 'all' + - numpy ; extra == 'all' + - pandas ; extra == 'all' + - pyarrow ; extra == 'all' + - adbc-driver-manager ; extra == 'all' + requires_python: '>=3.10.0' +- pypi: https://files.pythonhosted.org/packages/e0/96/4460429651e371eb5ff745a4790e7fa0509c7a58c71fc4f0f893404c9646/duckdb-1.5.0-cp313-cp313-win_amd64.whl + name: duckdb + version: 1.5.0 + sha256: 9a3d3dfa2d8bc74008ce3ad9564761ae23505a9e4282f6a36df29bd87249620b + requires_dist: + - ipython ; extra == 'all' + - fsspec ; extra == 'all' + - numpy ; extra == 'all' + - pandas ; extra == 'all' + - pyarrow ; extra == 'all' + - adbc-driver-manager ; extra == 'all' + requires_python: '>=3.10.0' +- conda: https://conda.anaconda.org/conda-forge/noarch/exceptiongroup-1.3.1-pyhd8ed1ab_0.conda + sha256: ee6cf346d017d954255bbcbdb424cddea4d14e4ed7e9813e429db1d795d01144 + md5: 8e662bd460bda79b1ea39194e3c4c9ab + depends: + - python >=3.10 + - typing_extensions >=4.6.0 + license: MIT and PSF-2.0 + purls: + - pkg:pypi/exceptiongroup?source=hash-mapping + size: 21333 + timestamp: 1763918099466 +- conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.3-h33c6efd_0.conda + sha256: fbf86c4a59c2ed05bbffb2ba25c7ed94f6185ec30ecb691615d42342baa1a16a + md5: c80d8a3b84358cb967fa81e7075fbc8a + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libstdcxx >=14 + license: MIT + license_family: MIT + purls: [] + size: 12723451 + timestamp: 1773822285671 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/icu-78.3-hcab7f73_0.conda + sha256: 49ba6aed2c6b482bb0ba41078057555d29764299bc947b990708617712ef6406 + md5: 546da38c2fa9efacf203e2ad3f987c59 + depends: + - libgcc >=14 + - libstdcxx >=14 + license: MIT + license_family: MIT + purls: [] + size: 12837286 + timestamp: 1773822650615 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/icu-78.3-hef89b57_0.conda + sha256: 3a7907a17e9937d3a46dfd41cffaf815abad59a569440d1e25177c15fd0684e5 + md5: f1182c91c0de31a7abd40cedf6a5ebef + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: [] + size: 12361647 + timestamp: 1773822915649 +- pypi: https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl + name: idna + version: '3.11' + sha256: 771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea + requires_dist: + - ruff>=0.6.2 ; extra == 'all' + - mypy>=1.11.2 ; extra == 'all' + - pytest>=8.3.2 ; extra == 'all' + - flake8>=7.1.1 ; extra == 'all' + requires_python: '>=3.8' +- conda: https://conda.anaconda.org/conda-forge/noarch/iniconfig-2.3.0-pyhd8ed1ab_0.conda + sha256: e1a9e3b1c8fe62dc3932a616c284b5d8cbe3124bbfbedcf4ce5c828cb166ee19 + md5: 9614359868482abba1bd15ce465e3c42 + depends: + - python >=3.10 + license: MIT + license_family: MIT + purls: + - pkg:pypi/iniconfig?source=compressed-mapping + size: 13387 + timestamp: 1760831448842 +- pypi: https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl + name: jinja2 + version: 3.1.6 + sha256: 85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67 + requires_dist: + - markupsafe>=2.0 + - babel>=2.7 ; extra == 'i18n' + requires_python: '>=3.7' +- conda: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.45.1-default_hbd61a6d_101.conda + sha256: 565941ac1f8b0d2f2e8f02827cbca648f4d18cd461afc31f15604cd291b5c5f3 + md5: 12bd9a3f089ee6c9266a37dab82afabd + depends: + - __glibc >=2.17,<3.0.a0 + - zstd >=1.5.7,<1.6.0a0 + constrains: + - binutils_impl_linux-64 2.45.1 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 725507 + timestamp: 1770267139900 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ld_impl_linux-aarch64-2.45.1-default_h1979696_101.conda + sha256: 44527364aa333be631913451c32eb0cae1e09343827e9ce3ccabd8d962584226 + md5: 35b2ae7fadf364b8e5fb8185aaeb80e5 + depends: + - zstd >=1.5.7,<1.6.0a0 + constrains: + - binutils_impl_linux-aarch64 2.45.1 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 875924 + timestamp: 1770267209884 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.7.4-hecca717_0.conda + sha256: d78f1d3bea8c031d2f032b760f36676d87929b18146351c4464c66b0869df3f5 + md5: e7f7ce06ec24cfcfb9e36d28cf82ba57 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + constrains: + - expat 2.7.4.* + license: MIT + license_family: MIT + purls: [] + size: 76798 + timestamp: 1771259418166 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libexpat-2.7.4-hfae3067_0.conda + sha256: 995ce3ad96d0f4b5ed6296b051a0d7b6377718f325bc0e792fbb96b0e369dad7 + md5: 57f3b3da02a50a1be2a6fe847515417d + depends: + - libgcc >=14 + constrains: + - expat 2.7.4.* + license: MIT + license_family: MIT + purls: [] + size: 76564 + timestamp: 1771259530958 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libexpat-2.7.4-hf6b4638_0.conda + sha256: 03887d8080d6a8fe02d75b80929271b39697ecca7628f0657d7afaea87761edf + md5: a92e310ae8dfc206ff449f362fc4217f + depends: + - __osx >=11.0 + constrains: + - expat 2.7.4.* + license: MIT + license_family: MIT + purls: [] + size: 68199 + timestamp: 1771260020767 +- conda: https://conda.anaconda.org/conda-forge/win-64/libexpat-2.7.4-hac47afa_0.conda + sha256: b31f6fb629c4e17885aaf2082fb30384156d16b48b264e454de4a06a313b533d + md5: 1c1ced969021592407f16ada4573586d + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - expat 2.7.4.* + license: MIT + license_family: MIT + purls: [] + size: 70323 + timestamp: 1771259521393 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libffi-3.5.2-h3435931_0.conda + sha256: 31f19b6a88ce40ebc0d5a992c131f57d919f73c0b92cd1617a5bec83f6e961e6 + md5: a360c33a5abe61c07959e449fa1453eb + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + size: 58592 + timestamp: 1769456073053 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libffi-3.5.2-h376a255_0.conda + sha256: 3df4c539449aabc3443bbe8c492c01d401eea894603087fca2917aa4e1c2dea9 + md5: 2f364feefb6a7c00423e80dcb12db62a + depends: + - libgcc >=14 + license: MIT + license_family: MIT + purls: [] + size: 55952 + timestamp: 1769456078358 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libffi-3.5.2-hcf2aa1b_0.conda + sha256: 6686a26466a527585e6a75cc2a242bf4a3d97d6d6c86424a441677917f28bec7 + md5: 43c04d9cb46ef176bb2a4c77e324d599 + depends: + - __osx >=11.0 + license: MIT + license_family: MIT + purls: [] + size: 40979 + timestamp: 1769456747661 +- conda: https://conda.anaconda.org/conda-forge/win-64/libffi-3.5.2-h3d046cb_0.conda + sha256: 59d01f2dfa8b77491b5888a5ab88ff4e1574c9359f7e229da254cdfe27ddc190 + md5: 720b39f5ec0610457b725eb3f396219a + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: MIT + license_family: MIT + purls: [] + size: 45831 + timestamp: 1769456418774 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgcc-15.2.0-he0feb66_18.conda + sha256: faf7d2017b4d718951e3a59d081eb09759152f93038479b768e3d612688f83f5 + md5: 0aa00f03f9e39fb9876085dee11a85d4 + depends: + - __glibc >=2.17,<3.0.a0 + - _openmp_mutex >=4.5 + constrains: + - libgcc-ng ==15.2.0=*_18 + - libgomp 15.2.0 he0feb66_18 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 1041788 + timestamp: 1771378212382 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgcc-15.2.0-h8acb6b2_18.conda + sha256: 43df385bedc1cab11993c4369e1f3b04b4ca5d0ea16cba6a0e7f18dbc129fcc9 + md5: 552567ea2b61e3a3035759b2fdb3f9a6 + depends: + - _openmp_mutex >=4.5 + constrains: + - libgcc-ng ==15.2.0=*_18 + - libgomp 15.2.0 h8acb6b2_18 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 622900 + timestamp: 1771378128706 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libgomp-15.2.0-he0feb66_18.conda + sha256: 21337ab58e5e0649d869ab168d4e609b033509de22521de1bfed0c031bfc5110 + md5: 239c5e9546c38a1e884d69effcf4c882 + depends: + - __glibc >=2.17,<3.0.a0 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 603262 + timestamp: 1771378117851 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libgomp-15.2.0-h8acb6b2_18.conda + sha256: fc716f11a6a8525e27a5d332ef6a689210b0d2a4dd1133edc0f530659aa9faa6 + md5: 4faa39bf919939602e594253bd673958 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 588060 + timestamp: 1771378040807 +- conda: https://conda.anaconda.org/conda-forge/linux-64/liblzma-5.8.2-hb03c661_0.conda + sha256: 755c55ebab181d678c12e49cced893598f2bab22d582fbbf4d8b83c18be207eb + md5: c7c83eecbb72d88b940c249af56c8b17 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + constrains: + - xz 5.8.2.* + license: 0BSD + purls: [] + size: 113207 + timestamp: 1768752626120 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/liblzma-5.8.2-he30d5cf_0.conda + sha256: 843c46e20519651a3e357a8928352b16c5b94f4cd3d5481acc48be2e93e8f6a3 + md5: 96944e3c92386a12755b94619bae0b35 + depends: + - libgcc >=14 + constrains: + - xz 5.8.2.* + license: 0BSD + purls: [] + size: 125916 + timestamp: 1768754941722 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/liblzma-5.8.2-h8088a28_0.conda + sha256: 7bfc7ffb2d6a9629357a70d4eadeadb6f88fa26ebc28f606b1c1e5e5ed99dc7e + md5: 009f0d956d7bfb00de86901d16e486c7 + depends: + - __osx >=11.0 + constrains: + - xz 5.8.2.* + license: 0BSD + purls: [] + size: 92242 + timestamp: 1768752982486 +- conda: https://conda.anaconda.org/conda-forge/win-64/liblzma-5.8.2-hfd05255_0.conda + sha256: f25bf293f550c8ed2e0c7145eb404324611cfccff37660869d97abf526eb957c + md5: ba0bfd4c3cf73f299ffe46ff0eaeb8e3 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + constrains: + - xz 5.8.2.* + license: 0BSD + purls: [] + size: 106169 + timestamp: 1768752763559 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libmpdec-4.0.0-hb03c661_1.conda + sha256: fe171ed5cf5959993d43ff72de7596e8ac2853e9021dec0344e583734f1e0843 + md5: 2c21e66f50753a083cbe6b80f38268fa + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 92400 + timestamp: 1769482286018 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libmpdec-4.0.0-he30d5cf_1.conda + sha256: 57c0dd12d506e84541c4e877898bd2a59cca141df493d34036f18b2751e0a453 + md5: 7b9813e885482e3ccb1fa212b86d7fd0 + depends: + - libgcc >=14 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 114056 + timestamp: 1769482343003 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libmpdec-4.0.0-h84a0fba_1.conda + sha256: 1089c7f15d5b62c622625ec6700732ece83be8b705da8c6607f4dabb0c4bd6d2 + md5: 57c4be259f5e0b99a5983799a228ae55 + depends: + - __osx >=11.0 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 73690 + timestamp: 1769482560514 +- conda: https://conda.anaconda.org/conda-forge/win-64/libmpdec-4.0.0-hfd05255_1.conda + sha256: 40dcd0b9522a6e0af72a9db0ced619176e7cfdb114855c7a64f278e73f8a7514 + md5: e4a9fc2bba3b022dad998c78856afe47 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: BSD-2-Clause + license_family: BSD + purls: [] + size: 89411 + timestamp: 1769482314283 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.52.0-hf4e2dac_0.conda + sha256: d716847b7deca293d2e49ed1c8ab9e4b9e04b9d780aea49a97c26925b28a7993 + md5: fd893f6a3002a635b5e50ceb9dd2c0f4 + depends: + - __glibc >=2.17,<3.0.a0 + - icu >=78.2,<79.0a0 + - libgcc >=14 + - libzlib >=1.3.1,<2.0a0 + license: blessing + purls: [] + size: 951405 + timestamp: 1772818874251 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libsqlite-3.52.0-h10b116e_0.conda + sha256: 1ddaf91b44fae83856276f4cb7ce544ffe41d4b55c1e346b504c6b45f19098d6 + md5: 77891484f18eca74b8ad83694da9815e + depends: + - icu >=78.2,<79.0a0 + - libgcc >=14 + - libzlib >=1.3.1,<2.0a0 + license: blessing + purls: [] + size: 952296 + timestamp: 1772818881550 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libsqlite-3.52.0-h1ae2325_0.conda + sha256: beb0fd5594d6d7c7cd42c992b6bb4d66cbb39d6c94a8234f15956da99a04306c + md5: f6233a3fddc35a2ec9f617f79d6f3d71 + depends: + - __osx >=11.0 + - icu >=78.2,<79.0a0 + - libzlib >=1.3.1,<2.0a0 + license: blessing + purls: [] + size: 918420 + timestamp: 1772819478684 +- conda: https://conda.anaconda.org/conda-forge/win-64/libsqlite-3.52.0-hf5d6505_0.conda + sha256: 5fccf1e4e4062f8b9a554abf4f9735a98e70f82e2865d0bfdb47b9de94887583 + md5: 8830689d537fda55f990620680934bb1 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: blessing + purls: [] + size: 1297302 + timestamp: 1772818899033 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-15.2.0-h934c35e_18.conda + sha256: 78668020064fdaa27e9ab65cd2997e2c837b564ab26ce3bf0e58a2ce1a525c6e + md5: 1b08cd684f34175e4514474793d44bcb + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc 15.2.0 he0feb66_18 + constrains: + - libstdcxx-ng ==15.2.0=*_18 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 5852330 + timestamp: 1771378262446 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libstdcxx-15.2.0-hef695bb_18.conda + sha256: 31fdb9ffafad106a213192d8319b9f810e05abca9c5436b60e507afb35a6bc40 + md5: f56573d05e3b735cb03efeb64a15f388 + depends: + - libgcc 15.2.0 h8acb6b2_18 + constrains: + - libstdcxx-ng ==15.2.0=*_18 + license: GPL-3.0-only WITH GCC-exception-3.1 + license_family: GPL + purls: [] + size: 5541411 + timestamp: 1771378162499 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libuuid-2.41.3-h5347b49_0.conda + sha256: 1a7539cfa7df00714e8943e18de0b06cceef6778e420a5ee3a2a145773758aee + md5: db409b7c1720428638e7c0d509d3e1b5 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 40311 + timestamp: 1766271528534 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libuuid-2.41.3-h1022ec0_0.conda + sha256: c37a8e89b700646f3252608f8368e7eb8e2a44886b92776e57ad7601fc402a11 + md5: cf2861212053d05f27ec49c3784ff8bb + depends: + - libgcc >=14 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 43453 + timestamp: 1766271546875 +- conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-hb9d3cd8_2.conda + sha256: d4bfe88d7cb447768e31650f06257995601f89076080e76df55e3112d4e47dc4 + md5: edb0dca6bc32e4f4789199455a1dbeb8 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + constrains: + - zlib 1.3.1 *_2 + license: Zlib + license_family: Other + purls: [] + size: 60963 + timestamp: 1727963148474 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/libzlib-1.3.1-h86ecc28_2.conda + sha256: 5a2c1eeef69342e88a98d1d95bff1603727ab1ff4ee0e421522acd8813439b84 + md5: 08aad7cbe9f5a6b460d0976076b6ae64 + depends: + - libgcc >=13 + constrains: + - zlib 1.3.1 *_2 + license: Zlib + license_family: Other + purls: [] + size: 66657 + timestamp: 1727963199518 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-h8359307_2.conda + sha256: ce34669eadaba351cd54910743e6a2261b67009624dbc7daeeafdef93616711b + md5: 369964e85dc26bfe78f41399b366c435 + depends: + - __osx >=11.0 + constrains: + - zlib 1.3.1 *_2 + license: Zlib + license_family: Other + purls: [] + size: 46438 + timestamp: 1727963202283 +- conda: https://conda.anaconda.org/conda-forge/win-64/libzlib-1.3.1-h2466b09_2.conda + sha256: ba945c6493449bed0e6e29883c4943817f7c79cbff52b83360f7b341277c6402 + md5: 41fbfac52c601159df6c01f875de31b9 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.2,<15 + - vc14_runtime >=14.29.30139 + constrains: + - zlib 1.3.1 *_2 + license: Zlib + license_family: Other + purls: [] + size: 55476 + timestamp: 1727963768015 +- pypi: https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + name: markupsafe + version: 3.0.3 + sha256: 133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl + name: markupsafe + version: 3.0.3 + sha256: 9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl + name: markupsafe + version: 3.0.3 + sha256: 116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: markupsafe + version: 3.0.3 + sha256: ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676 + requires_python: '>=3.9' +- conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h2d0b736_3.conda + sha256: 3fde293232fa3fca98635e1167de6b7c7fda83caf24b9d6c91ec9eefb4f4d586 + md5: 47e340acb35de30501a76c7c799c41d7 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: X11 AND BSD-3-Clause + purls: [] + size: 891641 + timestamp: 1738195959188 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ncurses-6.5-ha32ae93_3.conda + sha256: 91cfb655a68b0353b2833521dc919188db3d8a7f4c64bea2c6a7557b24747468 + md5: 182afabe009dc78d8b73100255ee6868 + depends: + - libgcc >=13 + license: X11 AND BSD-3-Clause + purls: [] + size: 926034 + timestamp: 1738196018799 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/ncurses-6.5-h5e97a16_3.conda + sha256: 2827ada40e8d9ca69a153a45f7fd14f32b2ead7045d3bbb5d10964898fe65733 + md5: 068d497125e4bf8a66bf707254fff5ae + depends: + - __osx >=11.0 + license: X11 AND BSD-3-Clause + purls: [] + size: 797030 + timestamp: 1738196177597 +- conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.1-h35e630c_1.conda + sha256: 44c877f8af015332a5d12f5ff0fb20ca32f896526a7d0cdb30c769df1144fb5c + md5: f61eb8cd60ff9057122a3d338b99c00f + depends: + - __glibc >=2.17,<3.0.a0 + - ca-certificates + - libgcc >=14 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 3164551 + timestamp: 1769555830639 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/openssl-3.6.1-h546c87b_1.conda + sha256: 7f8048c0e75b2620254218d72b4ae7f14136f1981c5eb555ef61645a9344505f + md5: 25f5885f11e8b1f075bccf4a2da91c60 + depends: + - ca-certificates + - libgcc >=14 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 3692030 + timestamp: 1769557678657 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/openssl-3.6.1-hd24854e_1.conda + sha256: 361f5c5e60052abc12bdd1b50d7a1a43e6a6653aab99a2263bf2288d709dcf67 + md5: f4f6ad63f98f64191c3e77c5f5f29d76 + depends: + - __osx >=11.0 + - ca-certificates + license: Apache-2.0 + license_family: Apache + purls: [] + size: 3104268 + timestamp: 1769556384749 +- conda: https://conda.anaconda.org/conda-forge/win-64/openssl-3.6.1-hf411b9b_1.conda + sha256: 53a5ad2e5553b8157a91bb8aa375f78c5958f77cb80e9d2ce59471ea8e5c0bd6 + md5: eb585509b815415bc964b2c7e11c7eb3 + depends: + - ca-certificates + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Apache-2.0 + license_family: Apache + purls: [] + size: 9343023 + timestamp: 1769557547888 +- conda: https://conda.anaconda.org/conda-forge/noarch/packaging-26.0-pyhcf101f3_0.conda + sha256: c1fc0f953048f743385d31c468b4a678b3ad20caffdeaa94bed85ba63049fd58 + md5: b76541e68fea4d511b1ac46a28dcd2c6 + depends: + - python >=3.8 + - python + license: Apache-2.0 + license_family: APACHE + purls: + - pkg:pypi/packaging?source=compressed-mapping + size: 72010 + timestamp: 1769093650580 +- pypi: https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl + name: platformdirs + version: 4.9.4 + sha256: 68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868 + requires_python: '>=3.10' +- conda: https://conda.anaconda.org/conda-forge/noarch/pluggy-1.6.0-pyhf9edf01_1.conda + sha256: e14aafa63efa0528ca99ba568eaf506eb55a0371d12e6250aaaa61718d2eb62e + md5: d7585b6550ad04c8c5e21097ada2888e + depends: + - python >=3.9 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/pluggy?source=compressed-mapping + size: 25877 + timestamp: 1764896838868 +- pypi: https://files.pythonhosted.org/packages/47/10/2cbe4c6f0fb83d2de37249567373d64327a5e4d8db72f486db42875b08f6/pyarrow-23.0.1-cp313-cp313-macosx_12_0_arm64.whl + name: pyarrow + version: 23.0.1 + sha256: 6b8fda694640b00e8af3c824f99f789e836720aa8c9379fb435d4c4953a756b8 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/b3/93/10a48b5e238de6d562a411af6467e71e7aedbc9b87f8d3a35f1560ae30fb/pyarrow-23.0.1-cp313-cp313-manylinux_2_28_x86_64.whl + name: pyarrow + version: 23.0.1 + sha256: 9b6f4f17b43bc39d56fec96e53fe89d94bac3eb134137964371b45352d40d0c2 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/d5/09/a532297c9591a727d67760e2e756b83905dd89adb365a7f6e9c72578bcc1/pyarrow-23.0.1-cp313-cp313-win_amd64.whl + name: pyarrow + version: 23.0.1 + sha256: cecfb12ef629cf6be0b1887f9f86463b0dd3dc3195ae6224e74006be4736035a + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/f9/63/d2747d930882c9d661e9398eefc54f15696547b8983aaaf11d4a2e8b5426/pyarrow-23.0.1-cp313-cp313-manylinux_2_28_aarch64.whl + name: pyarrow + version: 23.0.1 + sha256: 71c5be5cbf1e1cb6169d2a0980850bccb558ddc9b747b6206435313c47c37677 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl + name: pycparser + version: '3.0' + sha256: b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 + requires_python: '>=3.10' +- pypi: https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl + name: pydantic + version: 2.12.5 + sha256: e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d + requires_dist: + - annotated-types>=0.6.0 + - pydantic-core==2.41.5 + - typing-extensions>=4.14.1 + - typing-inspection>=0.4.2 + - email-validator>=2.0.0 ; extra == 'email' + - tzdata ; python_full_version >= '3.9' and sys_platform == 'win32' and extra == 'timezone' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + name: pydantic-core + version: 2.41.5 + sha256: 0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0 + requires_dist: + - typing-extensions>=4.14.1 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl + name: pydantic-core + version: 2.41.5 + sha256: 112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34 + requires_dist: + - typing-extensions>=4.14.1 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl + name: pydantic-core + version: 2.41.5 + sha256: 79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11 + requires_dist: + - typing-extensions>=4.14.1 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: pydantic-core + version: 2.41.5 + sha256: 406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586 + requires_dist: + - typing-extensions>=4.14.1 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/6d/01/98f74ecbe92f042d27e4de3cd7f093422d523cc67fdc74e6a65dbe4efbb8/pygit2-1.19.1-cp313-cp313-win_amd64.whl + name: pygit2 + version: 1.19.1 + sha256: 6d73aedffad280f6b655394e303533fcff15545d4d8f322011179c9474bb1b13 + requires_dist: + - cffi>=2.0 + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/a7/02/02f0f56b9b0b044018d9047adf68ba842ebda662ba43ace942ed904f8e9d/pygit2-1.19.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl + name: pygit2 + version: 1.19.1 + sha256: cb4da746c92e23281890e865887d83f24e662fc3e1c481420e4993c5a13203fe + requires_dist: + - cffi>=2.0 + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/d5/80/1a87f6e043e04cfa125380a73ef9f87a8c58292b7d4a6ed2e6203b4cd534/pygit2-1.19.1-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl + name: pygit2 + version: 1.19.1 + sha256: ef18f1208422d3cac1c109417a5fc6143704cfff8e5de4e1665fa4a89ffe3902 + requires_dist: + - cffi>=2.0 + requires_python: '>=3.11' +- pypi: https://files.pythonhosted.org/packages/e2/1f/f67ec7f78a34ed14dbd3acf05ed23c4c8c2336ba6f3ca78d6b9962878435/pygit2-1.19.1-cp313-cp313-macosx_11_0_arm64.whl + name: pygit2 + version: 1.19.1 + sha256: ed39106f1d9560709191093ed5251471dfb6b9e4aa35299dde45f4b91f7c984e + requires_dist: + - cffi>=2.0 + requires_python: '>=3.11' +- conda: https://conda.anaconda.org/conda-forge/noarch/pygments-2.19.2-pyhd8ed1ab_0.conda + sha256: 5577623b9f6685ece2697c6eb7511b4c9ac5fb607c9babc2646c811b428fd46a + md5: 6b6ece66ebcae2d5f326c77ef2c5a066 + depends: + - python >=3.9 + license: BSD-2-Clause + license_family: BSD + purls: + - pkg:pypi/pygments?source=hash-mapping + size: 889287 + timestamp: 1750615908735 +- conda: https://conda.anaconda.org/conda-forge/noarch/pytest-9.0.2-pyhcf101f3_0.conda + sha256: 9e749fb465a8bedf0184d8b8996992a38de351f7c64e967031944978de03a520 + md5: 2b694bad8a50dc2f712f5368de866480 + depends: + - pygments >=2.7.2 + - python >=3.10 + - iniconfig >=1.0.1 + - packaging >=22 + - pluggy >=1.5,<2 + - tomli >=1 + - colorama >=0.4 + - exceptiongroup >=1 + - python + constrains: + - pytest-faulthandler >=2 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pytest?source=hash-mapping + size: 299581 + timestamp: 1765062031645 +- conda: https://conda.anaconda.org/conda-forge/noarch/pytest-lazy-fixtures-1.4.0-pyhd8ed1ab_0.conda + sha256: d8299885069ea20b0e48c4d6b97b2f508c26040e5740c9ce8b3d8abae4d68652 + md5: 04673c65f0c6cf3c2fe3927df657d0ee + depends: + - pytest >=7 + - python >=3.10,<4.0.0 + license: MIT + license_family: MIT + purls: + - pkg:pypi/pytest-lazy-fixtures?source=hash-mapping + size: 14600 + timestamp: 1758096611127 +- conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.13.12-hc97d973_100_cp313.conda + build_number: 100 + sha256: 8a08fe5b7cb5a28aa44e2994d18dbf77f443956990753a4ca8173153ffb6eb56 + md5: 4c875ed0e78c2d407ec55eadffb8cf3d + depends: + - __glibc >=2.17,<3.0.a0 + - bzip2 >=1.0.8,<2.0a0 + - ld_impl_linux-64 >=2.36.1 + - libexpat >=2.7.3,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - libgcc >=14 + - liblzma >=5.8.2,<6.0a0 + - libmpdec >=4.0.0,<5.0a0 + - libsqlite >=3.51.2,<4.0a0 + - libuuid >=2.41.3,<3.0a0 + - libzlib >=1.3.1,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.5.5,<4.0a0 + - python_abi 3.13.* *_cp313 + - readline >=8.3,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + license: Python-2.0 + purls: [] + size: 37364553 + timestamp: 1770272309861 + python_site_packages_path: lib/python3.13/site-packages +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/python-3.13.12-h4c0d347_100_cp313.conda + build_number: 100 + sha256: a6bdf48a245d70526b4e6a277a4b344ec3f7c787b358e5377d544ac9a303c111 + md5: 732a86d6786402b95e1dc68c32022500 + depends: + - bzip2 >=1.0.8,<2.0a0 + - ld_impl_linux-aarch64 >=2.36.1 + - libexpat >=2.7.3,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - libgcc >=14 + - liblzma >=5.8.2,<6.0a0 + - libmpdec >=4.0.0,<5.0a0 + - libsqlite >=3.51.2,<4.0a0 + - libuuid >=2.41.3,<3.0a0 + - libzlib >=1.3.1,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.5.5,<4.0a0 + - python_abi 3.13.* *_cp313 + - readline >=8.3,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + license: Python-2.0 + purls: [] + size: 33986700 + timestamp: 1770270924894 + python_site_packages_path: lib/python3.13/site-packages +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/python-3.13.12-h20e6be0_100_cp313.conda + build_number: 100 + sha256: 9a4f16a64def0853f0a7b6a7beb40d498fd6b09bee10b90c3d6069b664156817 + md5: 179c0f5ae4f22bc3be567298ed0b17b9 + depends: + - __osx >=11.0 + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.7.3,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - liblzma >=5.8.2,<6.0a0 + - libmpdec >=4.0.0,<5.0a0 + - libsqlite >=3.51.2,<4.0a0 + - libzlib >=1.3.1,<2.0a0 + - ncurses >=6.5,<7.0a0 + - openssl >=3.5.5,<4.0a0 + - python_abi 3.13.* *_cp313 + - readline >=8.3,<9.0a0 + - tk >=8.6.13,<8.7.0a0 + - tzdata + license: Python-2.0 + purls: [] + size: 12770674 + timestamp: 1770272314517 + python_site_packages_path: lib/python3.13/site-packages +- conda: https://conda.anaconda.org/conda-forge/win-64/python-3.13.12-h09917c8_100_cp313.conda + build_number: 100 + sha256: da70aec20ff5a5ae18bbba9fdd1e18190b419605cafaafb3bdad8becf11ce94d + md5: 4440c24966d0aa0c8f1e1d5006dac2d6 + depends: + - bzip2 >=1.0.8,<2.0a0 + - libexpat >=2.7.3,<3.0a0 + - libffi >=3.5.2,<3.6.0a0 + - liblzma >=5.8.2,<6.0a0 + - libmpdec >=4.0.0,<5.0a0 + - libsqlite >=3.51.2,<4.0a0 + - libzlib >=1.3.1,<2.0a0 + - openssl >=3.5.5,<4.0a0 + - python_abi 3.13.* *_cp313 + - tk >=8.6.13,<8.7.0a0 + - tzdata + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: Python-2.0 + purls: [] + size: 16535316 + timestamp: 1770270322707 + python_site_packages_path: Lib/site-packages +- conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.13-8_cp313.conda + build_number: 8 + sha256: 210bffe7b121e651419cb196a2a63687b087497595c9be9d20ebe97dd06060a7 + md5: 94305520c52a4aa3f6c2b1ff6008d9f8 + constrains: + - python 3.13.* *_cp313 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 7002 + timestamp: 1752805902938 +- conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda + sha256: 12ffde5a6f958e285aa22c191ca01bbd3d6e710aa852e00618fa6ddc59149002 + md5: d7d95fc8287ea7bf33e0e7116d2b95ec + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - ncurses >=6.5,<7.0a0 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 345073 + timestamp: 1765813471974 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/readline-8.3-hb682ff5_0.conda + sha256: fe695f9d215e9a2e3dd0ca7f56435ab4df24f5504b83865e3d295df36e88d216 + md5: 3d49cad61f829f4f0e0611547a9cda12 + depends: + - libgcc >=14 + - ncurses >=6.5,<7.0a0 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 357597 + timestamp: 1765815673644 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/readline-8.3-h46df422_0.conda + sha256: a77010528efb4b548ac2a4484eaf7e1c3907f2aec86123ed9c5212ae44502477 + md5: f8381319127120ce51e081dce4865cf4 + depends: + - __osx >=11.0 + - ncurses >=6.5,<7.0a0 + license: GPL-3.0-only + license_family: GPL + purls: [] + size: 313930 + timestamp: 1765813902568 +- pypi: https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl + name: requests + version: 2.32.5 + sha256: 2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 + requires_dist: + - charset-normalizer>=2,<4 + - idna>=2.5,<4 + - urllib3>=1.21.1,<3 + - certifi>=2017.4.17 + - pysocks>=1.5.6,!=1.5.7 ; extra == 'socks' + - chardet>=3.0.2,<6 ; extra == 'use-chardet-on-py3' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/af/fe/b6045c782f1fd1ae317d2a6ca1884857ce5c20f59befe6ab25a8603c43a7/ruamel_yaml-0.18.17-py3-none-any.whl + name: ruamel-yaml + version: 0.18.17 + sha256: 9c8ba9eb3e793efdf924b60d521820869d5bf0cb9c6f1b82d82de8295e290b9d + requires_dist: + - ruamel-yaml-clib>=0.2.15 ; python_full_version < '3.15' and platform_python_implementation == 'CPython' + - ruamel-yaml-jinja2>=0.2 ; extra == 'jinja2' + - ryd ; extra == 'docs' + - mercurial>5.7 ; extra == 'docs' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/06/c4/c124fbcef0684fcf3c9b72374c2a8c35c94464d8694c50f37eef27f5a145/ruamel_yaml_clib-0.2.15-cp313-cp313-win_amd64.whl + name: ruamel-yaml-clib + version: 0.2.15 + sha256: 45702dfbea1420ba3450bb3dd9a80b33f0badd57539c6aac09f42584303e0db6 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/ce/bb/6ef5abfa43b48dd55c30d53e997f8f978722f02add61efba31380d73e42e/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl + name: ruamel-yaml-clib + version: 0.2.15 + sha256: 3eb199178b08956e5be6288ee0b05b2fb0b5c1f309725ad25d9c6ea7e27f962a + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/d6/03/a1baa5b94f71383913f21b96172fb3a2eb5576a4637729adbf7cd9f797f8/ruamel_yaml_clib-0.2.15-cp313-cp313-macosx_11_0_arm64.whl + name: ruamel-yaml-clib + version: 0.2.15 + sha256: 65f48245279f9bb301d1276f9679b82e4c080a1ae25e679f682ac62446fac471 + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/ff/5d/e4f84c9c448613e12bd62e90b23aa127ea4c46b697f3d760acc32cb94f25/ruamel_yaml_clib-0.2.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl + name: ruamel-yaml-clib + version: 0.2.15 + sha256: 4d1032919280ebc04a80e4fb1e93f7a738129857eaec9448310e638c8bccefcf + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/c6/13/b57ab75b0f60b5ee8cb8924bc01a5c419ed3221e00f8f11f8c059a707eb7/sqlglot-30.0.3-py3-none-any.whl + name: sqlglot + version: 30.0.3 + sha256: 5489cc98b5666f1fafc21e0304ca286e513e142aa054ee5760806a2139d07a05 + requires_dist: + - duckdb>=0.6 ; extra == 'dev' + - sqlglot-mypy>=1.19.1.post1 ; extra == 'dev' + - setuptools-scm ; extra == 'dev' + - pandas ; extra == 'dev' + - pandas-stubs ; extra == 'dev' + - python-dateutil ; extra == 'dev' + - pytz ; extra == 'dev' + - pdoc ; extra == 'dev' + - pre-commit ; extra == 'dev' + - ruff==0.15.6 ; extra == 'dev' + - types-python-dateutil ; extra == 'dev' + - types-pytz ; extra == 'dev' + - typing-extensions ; extra == 'dev' + - pyperf ; extra == 'dev' + - sqlglotc==30.0.3 ; extra == 'c' + - sqlglotrs==0.13.0 ; extra == 'rs' + - sqlglotc==30.0.3 ; extra == 'rs' + requires_python: '>=3.9' +- conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h366c992_103.conda + sha256: cafeec44494f842ffeca27e9c8b0c27ed714f93ac77ddadc6aaf726b5554ebac + md5: cffd3bdd58090148f4cfcd831f4b26ab + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - libzlib >=1.3.1,<2.0a0 + constrains: + - xorg-libx11 >=1.8.12,<2.0a0 + license: TCL + license_family: BSD + purls: [] + size: 3301196 + timestamp: 1769460227866 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/tk-8.6.13-noxft_h0dc03b3_103.conda + sha256: e25c314b52764219f842b41aea2c98a059f06437392268f09b03561e4f6e5309 + md5: 7fc6affb9b01e567d2ef1d05b84aa6ed + depends: + - libgcc >=14 + - libzlib >=1.3.1,<2.0a0 + constrains: + - xorg-libx11 >=1.8.12,<2.0a0 + license: TCL + license_family: BSD + purls: [] + size: 3368666 + timestamp: 1769464148928 +- conda: https://conda.anaconda.org/conda-forge/osx-arm64/tk-8.6.13-h010d191_3.conda + sha256: 799cab4b6cde62f91f750149995d149bc9db525ec12595e8a1d91b9317f038b3 + md5: a9d86bc62f39b94c4661716624eb21b0 + depends: + - __osx >=11.0 + - libzlib >=1.3.1,<2.0a0 + license: TCL + license_family: BSD + purls: [] + size: 3127137 + timestamp: 1769460817696 +- conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h6ed50ae_3.conda + sha256: 0e79810fae28f3b69fe7391b0d43f5474d6bd91d451d5f2bde02f55ae481d5e3 + md5: 0481bfd9814bf525bd4b3ee4b51494c4 + depends: + - ucrt >=10.0.20348.0 + - vc >=14.3,<15 + - vc14_runtime >=14.44.35208 + license: TCL + license_family: BSD + purls: [] + size: 3526350 + timestamp: 1769460339384 +- conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.0-pyhcf101f3_0.conda + sha256: 62940c563de45790ba0f076b9f2085a842a65662268b02dd136a8e9b1eaf47a8 + md5: 72e780e9aa2d0a3295f59b1874e3768b + depends: + - python >=3.10 + - python + license: MIT + license_family: MIT + purls: + - pkg:pypi/tomli?source=compressed-mapping + size: 21453 + timestamp: 1768146676791 +- pypi: https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl + name: tomlkit + version: 0.13.3 + sha256: c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0 + requires_python: '>=3.8' +- pypi: https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl + name: typing-inspection + version: 0.4.2 + sha256: 4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 + requires_dist: + - typing-extensions>=4.12.0 + requires_python: '>=3.9' +- conda: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.15.0-pyhcf101f3_0.conda + sha256: 032271135bca55aeb156cee361c81350c6f3fb203f57d024d7e5a1fc9ef18731 + md5: 0caa1af407ecff61170c9437a808404d + depends: + - python >=3.10 + - python + license: PSF-2.0 + license_family: PSF + purls: + - pkg:pypi/typing-extensions?source=hash-mapping + size: 51692 + timestamp: 1756220668932 +- pypi: https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl + name: tzdata + version: '2025.3' + sha256: 06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1 + requires_python: '>=2' +- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda + sha256: 1d30098909076af33a35017eed6f2953af1c769e273a0626a04722ac4acaba3c + md5: ad659d0a2b3e47e38d829aa8cad2d610 + license: LicenseRef-Public-Domain + purls: [] + size: 119135 + timestamp: 1767016325805 +- pypi: https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl + name: tzlocal + version: 5.3.1 + sha256: eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d + requires_dist: + - tzdata ; sys_platform == 'win32' + - pytest>=4.3 ; extra == 'devenv' + - pytest-mock>=3.3 ; extra == 'devenv' + - pytest-cov ; extra == 'devenv' + - check-manifest ; extra == 'devenv' + - zest-releaser ; extra == 'devenv' + requires_python: '>=3.9' +- conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.26100.0-h57928b3_0.conda + sha256: 3005729dce6f3d3f5ec91dfc49fc75a0095f9cd23bab49efb899657297ac91a5 + md5: 71b24316859acd00bdb8b38f5e2ce328 + constrains: + - vc14_runtime >=14.29.30037 + - vs2015_runtime >=14.29.30037 + license: LicenseRef-MicrosoftWindowsSDK10 + purls: [] + size: 694692 + timestamp: 1756385147981 +- pypi: https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl + name: urllib3 + version: 2.6.3 + sha256: bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4 + requires_dist: + - brotli>=1.2.0 ; platform_python_implementation == 'CPython' and extra == 'brotli' + - brotlicffi>=1.2.0.0 ; platform_python_implementation != 'CPython' and extra == 'brotli' + - h2>=4,<5 ; extra == 'h2' + - pysocks>=1.5.6,!=1.5.7,<2.0 ; extra == 'socks' + - backports-zstd>=1.0.0 ; python_full_version < '3.14' and extra == 'zstd' + requires_python: '>=3.9' +- conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h41ae7f8_34.conda + sha256: 9dc40c2610a6e6727d635c62cced5ef30b7b30123f5ef67d6139e23d21744b3a + md5: 1e610f2416b6acdd231c5f573d754a0f + depends: + - vc14_runtime >=14.44.35208 + track_features: + - vc14 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 19356 + timestamp: 1767320221521 +- conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.44.35208-h818238b_34.conda + sha256: 02732f953292cce179de9b633e74928037fa3741eb5ef91c3f8bae4f761d32a5 + md5: 37eb311485d2d8b2c419449582046a42 + depends: + - ucrt >=10.0.20348.0 + - vcomp14 14.44.35208 h818238b_34 + constrains: + - vs2015_runtime 14.44.35208.* *_34 + license: LicenseRef-MicrosoftVisualCpp2015-2022Runtime + license_family: Proprietary + purls: [] + size: 683233 + timestamp: 1767320219644 +- conda: https://conda.anaconda.org/conda-forge/win-64/vcomp14-14.44.35208-h818238b_34.conda + sha256: 878d5d10318b119bd98ed3ed874bd467acbe21996e1d81597a1dbf8030ea0ce6 + md5: 242d9f25d2ae60c76b38a5e42858e51d + depends: + - ucrt >=10.0.20348.0 + constrains: + - vs2015_runtime 14.44.35208.* *_34 + license: LicenseRef-MicrosoftVisualCpp2015-2022Runtime + license_family: Proprietary + purls: [] + size: 115235 + timestamp: 1767320173250 +- pypi: https://files.pythonhosted.org/packages/56/5c/8d6dc529595b5387f5727cd6c2c5b8615851d95fec5c599a61ef239cc1b3/whenever-0.9.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + name: whenever + version: 0.9.5 + sha256: b4056aaff273a579f0294e5397a0d198f52906bbaf7171da0a12ecd8cdf5026c + requires_dist: + - tzdata>=2020.1 ; sys_platform == 'win32' + - tzlocal>=4.0 ; sys_platform != 'darwin' and sys_platform != 'linux' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/81/b4/17d4bc76ca73c21eb5b7883d10d8bacb7ce7a30a8f36501db2373c63ffb3/whenever-0.9.5-cp313-cp313-win_amd64.whl + name: whenever + version: 0.9.5 + sha256: 16497a2b889aeeb0ee80a0d3b9ce14cdb63d7eb7d904e003aae3cd4ac67da1e8 + requires_dist: + - tzdata>=2020.1 ; sys_platform == 'win32' + - tzlocal>=4.0 ; sys_platform != 'darwin' and sys_platform != 'linux' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/b5/dc/090732e6e75f15a6084700d3247db6aa1f885971b637531529c62c4ba1c6/whenever-0.9.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl + name: whenever + version: 0.9.5 + sha256: 9ac83555db44e1fcfc032114f45c09af0ed9d641380672c8deb7f1131a0fd783 + requires_dist: + - tzdata>=2020.1 ; sys_platform == 'win32' + - tzlocal>=4.0 ; sys_platform != 'darwin' and sys_platform != 'linux' + requires_python: '>=3.9' +- pypi: https://files.pythonhosted.org/packages/ea/81/d59f0e226ef542fc4bc86567d7b9e2bf9016c353b1f83661ee3913a140a7/whenever-0.9.5-cp313-cp313-macosx_11_0_arm64.whl + name: whenever + version: 0.9.5 + sha256: e00bc8f93fa469c630aad9dfdc538587c28891d6a4dce2f0b08628d5a108a219 + requires_dist: + - tzdata>=2020.1 ; sys_platform == 'win32' + - tzlocal>=4.0 ; sys_platform != 'darwin' and sys_platform != 'linux' + requires_python: '>=3.9' +- conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda + sha256: 68f0206ca6e98fea941e5717cec780ed2873ffabc0e1ed34428c061e2c6268c7 + md5: 4a13eeac0b5c8e5b8ab496e6c4ddd829 + depends: + - __glibc >=2.17,<3.0.a0 + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 601375 + timestamp: 1764777111296 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zstd-1.5.7-h85ac4a6_6.conda + sha256: 569990cf12e46f9df540275146da567d9c618c1e9c7a0bc9d9cfefadaed20b75 + md5: c3655f82dcea2aa179b291e7099c1fcc + depends: + - libzlib >=1.3.1,<2.0a0 + license: BSD-3-Clause + license_family: BSD + purls: [] + size: 614429 + timestamp: 1764777145593 diff --git a/rust/adbc-snowflake/pixi.toml b/rust/adbc-snowflake/pixi.toml new file mode 100644 index 0000000..315ea5c --- /dev/null +++ b/rust/adbc-snowflake/pixi.toml @@ -0,0 +1,38 @@ +# Copyright (c) 2025-2026 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# !!!! AUTO-GENERATED FILE. DO NOT EDIT. !!!! +# USE adbc-gen-workflow (see adbc-drivers/dev) TO UPDATE THIS FILE. + +[workspace] +authors = ["ADBC Drivers Contributors"] +channels = ["conda-forge"] +name = "build" +platforms = ["linux-64", "osx-arm64", "win-64", "linux-aarch64"] +version = "0.1.0" + +[tasks] +make = "adbc-make run build DRIVER=snowflake VERBOSE=true IMPL_LANG=rust" +test = "cargo test" +release = "adbc-release" +validate = "pytest -vvs --junit-xml=validation-report.xml -rfEsxX validation/tests/" +gendocs = "python -m validation.tests.generate_documentation" + +[dependencies] +python = ">=3.13.5,<3.14" +pytest-lazy-fixtures = ">=1.3.2,<2" + +[pypi-dependencies] +adbc-drivers-dev = { git = "https://github.com/adbc-drivers/dev" } +adbc-drivers-validation = { git = "https://github.com/adbc-drivers/validation" } diff --git a/rust/adbc-snowflake/src/database.rs b/rust/adbc-snowflake/src/database.rs index d137c04..266fe41 100644 --- a/rust/adbc-snowflake/src/database.rs +++ b/rust/adbc-snowflake/src/database.rs @@ -73,6 +73,34 @@ fn adbc_db_opt_to_sf(key: &str, value: &OptionValue) -> Result { param_names::PRIVATE_KEY_PASSWORD.into() } + // Account geography + "adbc.snowflake.sql.region" => "region".to_string(), + // Auth extras + // The Okta authenticator URL is the authenticator value in sf_core. + "adbc.snowflake.sql.client_option.okta_url" => param_names::AUTHENTICATOR.into(), + "adbc.snowflake.sql.client_option.identity_provider" => "identity_provider".to_string(), + // Connection timeouts (stored as-is; sf_core will use them once supported) + "adbc.snowflake.sql.client_option.login_timeout" => "login_timeout".to_string(), + "adbc.snowflake.sql.client_option.request_timeout" => "request_timeout".to_string(), + "adbc.snowflake.sql.client_option.jwt_expire_timeout" => "jwt_expire_timeout".to_string(), + "adbc.snowflake.sql.client_option.client_timeout" => "client_timeout".to_string(), + // TLS — tls_skip_verify compound effect is applied separately in set_option + "adbc.snowflake.sql.client_option.tls_skip_verify" => "tls_skip_verify".to_string(), + "adbc.snowflake.sql.client_option.tls_root_cert" => { + param_names::CUSTOM_ROOT_STORE_PATH.into() + } + // OCSP — ocsp_fail_open_mode compound effect is applied separately in set_option + "adbc.snowflake.sql.client_option.ocsp_fail_open_mode" => { + "ocsp_fail_open_mode".to_string() + } + // Session behaviour + "adbc.snowflake.sql.client_option.keep_session_alive" => "keep_session_alive".to_string(), + "adbc.snowflake.sql.client_option.disable_telemetry" => "disable_telemetry".to_string(), + "adbc.snowflake.sql.client_option.cache_mfa_token" => "cache_mfa_token".to_string(), + "adbc.snowflake.sql.client_option.store_temp_creds" => "store_temp_creds".to_string(), + // Config / logging + "adbc.snowflake.sql.client_option.config_file" => "config_file".to_string(), + "adbc.snowflake.sql.client_option.tracing" => "log_level".to_string(), "adbc.snowflake.sql.uri.port" => { let port = match value { OptionValue::String(s) => s.parse::().map_err(|_| { @@ -159,6 +187,59 @@ impl Optionable for Database { ) .map_err(crate::error::api_error_to_adbc_error)?; } + + // tls_skip_verify: also drive the underlying verify_certificates / verify_hostname + // params so sf_core skips certificate and hostname checks when enabled. + if key_str == "adbc.snowflake.sql.client_option.tls_skip_verify" { + let skip = matches!(&value, OptionValue::String(s) if s == "enabled"); + let verify = Setting::Bool(!skip); + self.sf_settings.insert( + param_names::VERIFY_CERTIFICATES.as_str().to_string(), + verify.clone(), + ); + self.sf_settings + .insert(param_names::VERIFY_HOSTNAME.as_str().to_string(), verify.clone()); + self.inner + .runtime + .block_on(async { + self.inner + .sf + .database_set_option( + self.db_handle, + param_names::VERIFY_CERTIFICATES.into(), + verify.clone(), + ) + .await?; + self.inner + .sf + .database_set_option( + self.db_handle, + param_names::VERIFY_HOSTNAME.into(), + verify, + ) + .await + }) + .map_err(crate::error::api_error_to_adbc_error)?; + } + + // ocsp_fail_open_mode: map to sf_core's crl_check_mode + // enabled (fail-open / advisory) → ADVISORY; disabled (strict) → ENABLED. + if key_str == "adbc.snowflake.sql.client_option.ocsp_fail_open_mode" { + let fail_open = matches!(&value, OptionValue::String(s) if s == "enabled"); + let mode = + Setting::String(if fail_open { "ADVISORY" } else { "ENABLED" }.to_string()); + self.sf_settings + .insert(param_names::CRL_CHECK_MODE.as_str().to_string(), mode.clone()); + self.inner + .runtime + .block_on(self.inner.sf.database_set_option( + self.db_handle, + param_names::CRL_CHECK_MODE.into(), + mode, + )) + .map_err(crate::error::api_error_to_adbc_error)?; + } + Ok(()) } @@ -501,6 +582,153 @@ mod tests { ); } + #[test] + fn tls_skip_verify_enabled_clears_verify_flags() { + use sf_core::config::settings::Setting; + let mut db = make_db(); + db.set_option( + OptionDatabase::Other("adbc.snowflake.sql.client_option.tls_skip_verify".into()), + OptionValue::String("enabled".into()), + ) + .unwrap(); + // Round-trip + assert_eq!( + db.get_option_string(OptionDatabase::Other( + "adbc.snowflake.sql.client_option.tls_skip_verify".into() + )) + .unwrap(), + "enabled" + ); + // Compound: verify_certificates and verify_hostname must be false + assert_eq!( + db.sf_settings.get(param_names::VERIFY_CERTIFICATES.as_str()), + Some(&Setting::Bool(false)) + ); + assert_eq!( + db.sf_settings.get(param_names::VERIFY_HOSTNAME.as_str()), + Some(&Setting::Bool(false)) + ); + } + + #[test] + fn tls_skip_verify_disabled_restores_verify_flags() { + use sf_core::config::settings::Setting; + let mut db = make_db(); + // First enable, then disable + db.set_option( + OptionDatabase::Other("adbc.snowflake.sql.client_option.tls_skip_verify".into()), + OptionValue::String("enabled".into()), + ) + .unwrap(); + db.set_option( + OptionDatabase::Other("adbc.snowflake.sql.client_option.tls_skip_verify".into()), + OptionValue::String("disabled".into()), + ) + .unwrap(); + assert_eq!( + db.get_option_string(OptionDatabase::Other( + "adbc.snowflake.sql.client_option.tls_skip_verify".into() + )) + .unwrap(), + "disabled" + ); + assert_eq!( + db.sf_settings.get(param_names::VERIFY_CERTIFICATES.as_str()), + Some(&Setting::Bool(true)) + ); + assert_eq!( + db.sf_settings.get(param_names::VERIFY_HOSTNAME.as_str()), + Some(&Setting::Bool(true)) + ); + } + + #[test] + fn ocsp_fail_open_mode_enabled_maps_to_crl_advisory() { + use sf_core::config::settings::Setting; + let mut db = make_db(); + db.set_option( + OptionDatabase::Other( + "adbc.snowflake.sql.client_option.ocsp_fail_open_mode".into(), + ), + OptionValue::String("enabled".into()), + ) + .unwrap(); + assert_eq!( + db.get_option_string(OptionDatabase::Other( + "adbc.snowflake.sql.client_option.ocsp_fail_open_mode".into() + )) + .unwrap(), + "enabled" + ); + assert_eq!( + db.sf_settings.get(param_names::CRL_CHECK_MODE.as_str()), + Some(&Setting::String("ADVISORY".into())) + ); + } + + #[test] + fn ocsp_fail_open_mode_disabled_maps_to_crl_enabled() { + use sf_core::config::settings::Setting; + let mut db = make_db(); + db.set_option( + OptionDatabase::Other( + "adbc.snowflake.sql.client_option.ocsp_fail_open_mode".into(), + ), + OptionValue::String("disabled".into()), + ) + .unwrap(); + assert_eq!( + db.sf_settings.get(param_names::CRL_CHECK_MODE.as_str()), + Some(&Setting::String("ENABLED".into())) + ); + } + + #[test] + fn simple_option_round_trips() { + let mut db = make_db(); + let cases = [ + ( + "adbc.snowflake.sql.region", + "us-east-1", + ), + ( + "adbc.snowflake.sql.client_option.login_timeout", + "30s", + ), + ( + "adbc.snowflake.sql.client_option.request_timeout", + "60s", + ), + ( + "adbc.snowflake.sql.client_option.keep_session_alive", + "enabled", + ), + ( + "adbc.snowflake.sql.client_option.disable_telemetry", + "enabled", + ), + ( + "adbc.snowflake.sql.client_option.tracing", + "debug", + ), + ( + "adbc.snowflake.sql.client_option.config_file", + "/home/user/.snowflake/config.toml", + ), + ]; + for (key, val) in cases { + db.set_option( + OptionDatabase::Other(key.into()), + OptionValue::String(val.into()), + ) + .unwrap_or_else(|e| panic!("set_option({key}) failed: {e}")); + let got = db + .get_option_string(OptionDatabase::Other(key.into())) + .unwrap_or_else(|e| panic!("get_option_string({key}) failed: {e}")); + assert_eq!(got, val, "round-trip failed for {key}"); + } + } + #[test] fn uri_parses_account_user_database() { let mut db = make_db(); diff --git a/rust/adbc-snowflake/tests/integration.rs b/rust/adbc-snowflake/tests/integration.rs index 65d8e06..95b7344 100644 --- a/rust/adbc-snowflake/tests/integration.rs +++ b/rust/adbc-snowflake/tests/integration.rs @@ -470,3 +470,127 @@ fn test_timestamp_precision_get_table_schema() { stmt.execute_update().expect("drop ts precision test table"); } } + +// ── missing database options ────────────────────────────────────────────────── + +/// Verify simple 1:1 option round-trips through the public Database API. +/// No live connection required — make_db() only needs SNOWFLAKE_URI for the env +/// var check; no actual network call is made until new_connection(). +#[test] +fn test_database_options_round_trip() { + let Some(mut db) = make_db() else { + eprintln!("Skipping: SNOWFLAKE_URI not set"); + return; + }; + + let cases: &[(&str, &str)] = &[ + ("adbc.snowflake.sql.region", "us-east-1"), + ("adbc.snowflake.sql.client_option.login_timeout", "30s"), + ("adbc.snowflake.sql.client_option.request_timeout", "60s"), + ("adbc.snowflake.sql.client_option.jwt_expire_timeout", "90s"), + ("adbc.snowflake.sql.client_option.client_timeout", "120s"), + ( + "adbc.snowflake.sql.client_option.keep_session_alive", + "enabled", + ), + ("adbc.snowflake.sql.client_option.disable_telemetry", "enabled"), + ("adbc.snowflake.sql.client_option.cache_mfa_token", "enabled"), + ("adbc.snowflake.sql.client_option.store_temp_creds", "enabled"), + ("adbc.snowflake.sql.client_option.tracing", "debug"), + ( + "adbc.snowflake.sql.client_option.identity_provider", + "azure", + ), + ]; + + for (key, val) in cases { + db.set_option( + OptionDatabase::Other((*key).into()), + OptionValue::String((*val).into()), + ) + .unwrap_or_else(|e| panic!("set_option({key}) failed: {e}")); + + let got = db + .get_option_string(OptionDatabase::Other((*key).into())) + .unwrap_or_else(|e| panic!("get_option_string({key}) failed: {e}")); + + assert_eq!(got, *val, "round-trip mismatch for {key}"); + } +} + +/// Verify tls_skip_verify round-trips and that the compound TLS flags are +/// readable back through their own option keys. +#[test] +fn test_tls_skip_verify_option() { + let Some(mut db) = make_db() else { + eprintln!("Skipping: SNOWFLAKE_URI not set"); + return; + }; + + // enabled → skip verification + db.set_option( + OptionDatabase::Other("adbc.snowflake.sql.client_option.tls_skip_verify".into()), + OptionValue::String("enabled".into()), + ) + .expect("set tls_skip_verify enabled"); + assert_eq!( + db.get_option_string(OptionDatabase::Other( + "adbc.snowflake.sql.client_option.tls_skip_verify".into() + )) + .unwrap(), + "enabled" + ); + + // disabled → restore verification + db.set_option( + OptionDatabase::Other("adbc.snowflake.sql.client_option.tls_skip_verify".into()), + OptionValue::String("disabled".into()), + ) + .expect("set tls_skip_verify disabled"); + assert_eq!( + db.get_option_string(OptionDatabase::Other( + "adbc.snowflake.sql.client_option.tls_skip_verify".into() + )) + .unwrap(), + "disabled" + ); +} + +/// Verify ocsp_fail_open_mode round-trips correctly. +#[test] +fn test_ocsp_fail_open_mode_option() { + let Some(mut db) = make_db() else { + eprintln!("Skipping: SNOWFLAKE_URI not set"); + return; + }; + + db.set_option( + OptionDatabase::Other( + "adbc.snowflake.sql.client_option.ocsp_fail_open_mode".into(), + ), + OptionValue::String("enabled".into()), + ) + .expect("set ocsp_fail_open_mode enabled"); + assert_eq!( + db.get_option_string(OptionDatabase::Other( + "adbc.snowflake.sql.client_option.ocsp_fail_open_mode".into() + )) + .unwrap(), + "enabled" + ); + + db.set_option( + OptionDatabase::Other( + "adbc.snowflake.sql.client_option.ocsp_fail_open_mode".into(), + ), + OptionValue::String("disabled".into()), + ) + .expect("set ocsp_fail_open_mode disabled"); + assert_eq!( + db.get_option_string(OptionDatabase::Other( + "adbc.snowflake.sql.client_option.ocsp_fail_open_mode".into() + )) + .unwrap(), + "disabled" + ); +} From 686356db1f69cf0a7a9d8b3f5e00796332586239 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Fri, 20 Mar 2026 13:20:33 -0400 Subject: [PATCH 29/76] cleanup paths --- .github/workflows/rust_test.yaml | 42 +++++++++---------- rust/{adbc-snowflake => }/.cargo/config.toml | 0 rust/{adbc-snowflake => }/.gitattributes | 0 rust/{adbc-snowflake => }/.gitignore | 0 rust/{adbc-snowflake => }/Cargo.lock | 0 rust/{adbc-snowflake => }/Cargo.toml | 0 rust/{adbc-snowflake => }/manifest.toml | 0 rust/{adbc-snowflake => }/pixi.lock | 0 rust/{adbc-snowflake => }/pixi.toml | 0 rust/{adbc-snowflake => }/src/connection.rs | 0 rust/{adbc-snowflake => }/src/database.rs | 0 rust/{adbc-snowflake => }/src/driver.rs | 0 rust/{adbc-snowflake => }/src/error.rs | 0 rust/{adbc-snowflake => }/src/lib.rs | 0 rust/{adbc-snowflake => }/src/statement.rs | 0 .../{adbc-snowflake => }/tests/integration.rs | 0 16 files changed, 21 insertions(+), 21 deletions(-) rename rust/{adbc-snowflake => }/.cargo/config.toml (100%) rename rust/{adbc-snowflake => }/.gitattributes (100%) rename rust/{adbc-snowflake => }/.gitignore (100%) rename rust/{adbc-snowflake => }/Cargo.lock (100%) rename rust/{adbc-snowflake => }/Cargo.toml (100%) rename rust/{adbc-snowflake => }/manifest.toml (100%) rename rust/{adbc-snowflake => }/pixi.lock (100%) rename rust/{adbc-snowflake => }/pixi.toml (100%) rename rust/{adbc-snowflake => }/src/connection.rs (100%) rename rust/{adbc-snowflake => }/src/database.rs (100%) rename rust/{adbc-snowflake => }/src/driver.rs (100%) rename rust/{adbc-snowflake => }/src/error.rs (100%) rename rust/{adbc-snowflake => }/src/lib.rs (100%) rename rust/{adbc-snowflake => }/src/statement.rs (100%) rename rust/{adbc-snowflake => }/tests/integration.rs (100%) diff --git a/.github/workflows/rust_test.yaml b/.github/workflows/rust_test.yaml index a39493d..c5f6ef2 100644 --- a/.github/workflows/rust_test.yaml +++ b/.github/workflows/rust_test.yaml @@ -107,7 +107,7 @@ jobs: - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 with: - workspaces: rust/adbc-snowflake + workspaces: rust - uses: prefix-dev/setup-pixi@a0af7a228712d6121d37aba47adf55c1332c9c2e # v0.9.4 with: @@ -115,7 +115,7 @@ jobs: run-install: false - name: Build - working-directory: rust/adbc-snowflake + working-directory: rust run: | if [[ -f ci/scripts/pre-build.sh ]]; then echo "Loading pre-build" @@ -134,7 +134,7 @@ jobs: # Can't use Docker on macOS AArch64 runners, and Windows containers # work but often the container doesn't support Windows if: runner.os == 'Linux' - working-directory: rust/adbc-snowflake + working-directory: rust run: | if [[ -f compose.yaml ]]; then if ! docker compose up --detach --wait test-service; then @@ -146,7 +146,7 @@ jobs: fi - name: Test - working-directory: rust/adbc-snowflake + working-directory: rust env: SNOWFLAKE_DATABASE: ${{ secrets.SNOWFLAKE_DATABASE }} SNOWFLAKE_SCHEMA: ${{ secrets.SNOWFLAKE_SCHEMA }} @@ -180,7 +180,7 @@ jobs: - name: Lint if: runner.os == 'Linux' - working-directory: rust/adbc-snowflake + working-directory: rust run: | cargo fmt --check cargo clippy -- -D warnings @@ -230,7 +230,7 @@ jobs: - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 with: - workspaces: rust/adbc-snowflake + workspaces: rust - uses: prefix-dev/setup-pixi@a0af7a228712d6121d37aba47adf55c1332c9c2e # v0.9.4 with: @@ -245,7 +245,7 @@ jobs: echo "$GHCR_TOKEN" | docker login ghcr.io -u "$GITHUB_ACTOR" --password-stdin - name: Build Library - working-directory: rust/adbc-snowflake + working-directory: rust run: | if [[ -f ci/scripts/pre-build.sh ]]; then ./ci/scripts/pre-build.sh test ${{ matrix.platform }} ${{ matrix.arch }} @@ -265,7 +265,7 @@ jobs: # Can't use Docker on macOS AArch64 runners, and windows containers # work but often the container doesn't support Windows if: runner.os == 'Linux' - working-directory: rust/adbc-snowflake + working-directory: rust run: | if [[ -f compose.yaml ]]; then if ! docker compose up --detach --wait test-service; then @@ -285,7 +285,7 @@ jobs: SNOWFLAKE_SCHEMA_SECONDARY: ${{ secrets.SNOWFLAKE_SCHEMA_SECONDARY }} SNOWFLAKE_DATABASE_SECONDARY: ${{ secrets.SNOWFLAKE_DATABASE_SECONDARY }} SNOWFLAKE_DATABASE_SECONDARY_SCHEMA: ${{ secrets.SNOWFLAKE_DATABASE_SECONDARY_SCHEMA }} - working-directory: rust/adbc-snowflake + working-directory: rust run: | set -a if [[ -f .env ]]; then @@ -314,18 +314,18 @@ jobs: - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: validation-report - path: "rust/adbc-snowflake/validation-report.xml" + path: "rust/validation-report.xml" retention-days: 7 - name: Generate docs - working-directory: rust/adbc-snowflake + working-directory: rust run: | pixi run gendocs --output generated - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: docs - path: "rust/adbc-snowflake/generated/snowflake.md" + path: "rust/generated/snowflake.md" retention-days: 2 build: @@ -369,7 +369,7 @@ jobs: - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 with: - workspaces: rust/adbc-snowflake + workspaces: rust - uses: prefix-dev/setup-pixi@a0af7a228712d6121d37aba47adf55c1332c9c2e # v0.9.4 with: @@ -377,7 +377,7 @@ jobs: run-install: false - name: Install dev tools - working-directory: rust/adbc-snowflake + working-directory: rust run: | pixi install @@ -386,10 +386,10 @@ jobs: env: GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - echo "$GHCR_TOKEN" | docker login ghcr.io -u ${{ github.actor }} --password-stdin + echo "$GHCR_TOKEN" | docker login ghcr.io -u "$GITHUB_ACTOR" --password-stdin - name: Build Library - working-directory: rust/adbc-snowflake + working-directory: rust run: | if [[ -f ci/scripts/pre-build.sh ]]; then ./ci/scripts/pre-build.sh release ${{ matrix.platform }} ${{ matrix.arch }} @@ -408,7 +408,7 @@ jobs: - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: drivers-${{ matrix.platform }}-${{ matrix.arch }} - path: rust/adbc-snowflake/target/release/libadbc_snowflake.* + path: rust/target/release/libadbc_snowflake.* retention-days: 2 package: @@ -442,7 +442,7 @@ jobs: - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 with: - workspaces: rust/adbc-snowflake + workspaces: rust - uses: prefix-dev/setup-pixi@a0af7a228712d6121d37aba47adf55c1332c9c2e # v0.9.4 with: @@ -455,7 +455,7 @@ jobs: path: "~/drivers" - name: Generate packages - working-directory: rust/adbc-snowflake + working-directory: rust run: | pixi install @@ -529,7 +529,7 @@ jobs: persist-credentials: false submodules: 'recursive' - name: load package - working-directory: rust/adbc-snowflake + working-directory: rust run: | if [[ -f ci/scripts/pre-build.sh ]]; then echo "Loading pre-build" @@ -592,7 +592,7 @@ jobs: - name: Release (dry-run) env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - working-directory: rust/adbc-snowflake + working-directory: rust run: | git tag go/v1000.0.0 tag=go/v1000.0.0 diff --git a/rust/adbc-snowflake/.cargo/config.toml b/rust/.cargo/config.toml similarity index 100% rename from rust/adbc-snowflake/.cargo/config.toml rename to rust/.cargo/config.toml diff --git a/rust/adbc-snowflake/.gitattributes b/rust/.gitattributes similarity index 100% rename from rust/adbc-snowflake/.gitattributes rename to rust/.gitattributes diff --git a/rust/adbc-snowflake/.gitignore b/rust/.gitignore similarity index 100% rename from rust/adbc-snowflake/.gitignore rename to rust/.gitignore diff --git a/rust/adbc-snowflake/Cargo.lock b/rust/Cargo.lock similarity index 100% rename from rust/adbc-snowflake/Cargo.lock rename to rust/Cargo.lock diff --git a/rust/adbc-snowflake/Cargo.toml b/rust/Cargo.toml similarity index 100% rename from rust/adbc-snowflake/Cargo.toml rename to rust/Cargo.toml diff --git a/rust/adbc-snowflake/manifest.toml b/rust/manifest.toml similarity index 100% rename from rust/adbc-snowflake/manifest.toml rename to rust/manifest.toml diff --git a/rust/adbc-snowflake/pixi.lock b/rust/pixi.lock similarity index 100% rename from rust/adbc-snowflake/pixi.lock rename to rust/pixi.lock diff --git a/rust/adbc-snowflake/pixi.toml b/rust/pixi.toml similarity index 100% rename from rust/adbc-snowflake/pixi.toml rename to rust/pixi.toml diff --git a/rust/adbc-snowflake/src/connection.rs b/rust/src/connection.rs similarity index 100% rename from rust/adbc-snowflake/src/connection.rs rename to rust/src/connection.rs diff --git a/rust/adbc-snowflake/src/database.rs b/rust/src/database.rs similarity index 100% rename from rust/adbc-snowflake/src/database.rs rename to rust/src/database.rs diff --git a/rust/adbc-snowflake/src/driver.rs b/rust/src/driver.rs similarity index 100% rename from rust/adbc-snowflake/src/driver.rs rename to rust/src/driver.rs diff --git a/rust/adbc-snowflake/src/error.rs b/rust/src/error.rs similarity index 100% rename from rust/adbc-snowflake/src/error.rs rename to rust/src/error.rs diff --git a/rust/adbc-snowflake/src/lib.rs b/rust/src/lib.rs similarity index 100% rename from rust/adbc-snowflake/src/lib.rs rename to rust/src/lib.rs diff --git a/rust/adbc-snowflake/src/statement.rs b/rust/src/statement.rs similarity index 100% rename from rust/adbc-snowflake/src/statement.rs rename to rust/src/statement.rs diff --git a/rust/adbc-snowflake/tests/integration.rs b/rust/tests/integration.rs similarity index 100% rename from rust/adbc-snowflake/tests/integration.rs rename to rust/tests/integration.rs From f339b2a5ef9f8fee68856bdbb72f0104e72f015d Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Fri, 20 Mar 2026 13:27:39 -0400 Subject: [PATCH 30/76] add validation suite to rust impl --- rust/validation/README.md | 49 ++++++++ rust/validation/pytest.ini | 21 ++++ rust/validation/queries/ingest/binary.txtcase | 18 +++ .../queries/ingest/binary_view.toml | 16 +++ .../queries/ingest/decimal.input.json | 5 + .../queries/ingest/decimal.input.schema.json | 15 +++ .../queries/ingest/fixed_size_binary.txtcase | 18 +++ .../queries/ingest/float32.input.schema.json | 15 +++ .../queries/ingest/int16.input.schema.json | 15 +++ .../queries/ingest/int32.input.schema.json | 15 +++ .../queries/ingest/large_binary.txtcase | 18 +++ .../queries/ingest/large_string.txtcase | 18 +++ rust/validation/queries/ingest/string.txtcase | 18 +++ .../queries/ingest/string_view.toml | 16 +++ .../validation/queries/ingest/time_ms.txtcase | 18 +++ .../validation/queries/ingest/time_ns.txtcase | 18 +++ rust/validation/queries/ingest/time_s.txtcase | 18 +++ .../validation/queries/ingest/time_us.txtcase | 18 +++ .../queries/ingest/timestamp_ms.txtcase | 18 +++ .../queries/ingest/timestamp_ns.txtcase | 18 +++ .../queries/ingest/timestamp_s.txtcase | 18 +++ .../queries/ingest/timestamp_us.txtcase | 18 +++ .../queries/ingest/timestamptz_ms.txtcase | 18 +++ .../queries/ingest/timestamptz_ns.txtcase | 18 +++ .../queries/ingest/timestamptz_s.txtcase | 18 +++ .../queries/ingest/timestamptz_us.txtcase | 18 +++ rust/validation/queries/type/bind/binary.toml | 16 +++ .../queries/type/bind/binary_view.toml | 15 +++ rust/validation/queries/type/bind/date.toml | 16 +++ .../validation/queries/type/bind/decimal.toml | 15 +++ .../queries/type/bind/fixed_size_binary.toml | 15 +++ .../queries/type/bind/float16.txtcase | 16 +++ .../queries/type/bind/float32.schema.json | 10 ++ .../queries/type/bind/float64.bind.json | 6 + .../validation/queries/type/bind/float64.json | 6 + .../queries/type/bind/int16.schema.json | 10 ++ .../queries/type/bind/int32.schema.json | 10 ++ .../queries/type/bind/large_binary.toml | 15 +++ .../queries/type/bind/string_view.toml | 15 +++ .../queries/type/bind/time_ms.txtcase | 16 +++ .../queries/type/bind/time_ns.txtcase | 16 +++ .../queries/type/bind/time_s.txtcase | 16 +++ .../queries/type/bind/time_us.txtcase | 16 +++ .../queries/type/bind/timestamp_ms.txtcase | 16 +++ .../queries/type/bind/timestamp_ns.txtcase | 16 +++ .../queries/type/bind/timestamp_s.txtcase | 16 +++ .../queries/type/bind/timestamp_us.txtcase | 16 +++ .../queries/type/bind/timestamptz_ms.txtcase | 16 +++ .../queries/type/bind/timestamptz_ns.txtcase | 16 +++ .../queries/type/bind/timestamptz_s.txtcase | 16 +++ .../queries/type/bind/timestamptz_us.txtcase | 16 +++ .../queries/type/literal/binary.sql | 1 + .../queries/type/literal/decimal.json | 1 + .../queries/type/literal/decimal.schema.json | 10 ++ .../queries/type/literal/float32.schema.json | 10 ++ .../queries/type/literal/int16.schema.json | 10 ++ .../queries/type/literal/int32.schema.json | 10 ++ .../validation/queries/type/literal/time.json | 1 + .../queries/type/literal/time.schema.json | 10 ++ .../queries/type/literal/timestamp.sql | 1 + .../queries/type/literal/timestamptz.sql | 1 + .../queries/type/select/binary.setup.sql | 10 ++ .../queries/type/select/decimal.json | 5 + .../queries/type/select/decimal.schema.json | 10 ++ .../queries/type/select/float32.schema.json | 10 ++ .../queries/type/select/int16.schema.json | 10 ++ .../queries/type/select/int32.schema.json | 10 ++ .../queries/type/select/string.setup.sql | 10 ++ rust/validation/queries/type/select/time.json | 5 + .../queries/type/select/time.schema.json | 10 ++ .../queries/type/select/timestamp.setup.sql | 10 ++ .../queries/type/select/timestamptz.setup.sql | 10 ++ rust/validation/tests/.gitignore | 13 ++ rust/validation/tests/__init__.py | 15 +++ rust/validation/tests/conftest.py | 52 ++++++++ .../tests/generate_documentation.py | 36 ++++++ rust/validation/tests/snowflake.py | 94 +++++++++++++++ rust/validation/tests/test_connection.py | 25 ++++ rust/validation/tests/test_ingest.py | 27 +++++ rust/validation/tests/test_query.py | 113 ++++++++++++++++++ rust/validation/tests/test_statement.py | 27 +++++ 81 files changed, 1387 insertions(+) create mode 100644 rust/validation/README.md create mode 100644 rust/validation/pytest.ini create mode 100644 rust/validation/queries/ingest/binary.txtcase create mode 100644 rust/validation/queries/ingest/binary_view.toml create mode 100644 rust/validation/queries/ingest/decimal.input.json create mode 100644 rust/validation/queries/ingest/decimal.input.schema.json create mode 100644 rust/validation/queries/ingest/fixed_size_binary.txtcase create mode 100644 rust/validation/queries/ingest/float32.input.schema.json create mode 100644 rust/validation/queries/ingest/int16.input.schema.json create mode 100644 rust/validation/queries/ingest/int32.input.schema.json create mode 100644 rust/validation/queries/ingest/large_binary.txtcase create mode 100644 rust/validation/queries/ingest/large_string.txtcase create mode 100644 rust/validation/queries/ingest/string.txtcase create mode 100644 rust/validation/queries/ingest/string_view.toml create mode 100644 rust/validation/queries/ingest/time_ms.txtcase create mode 100644 rust/validation/queries/ingest/time_ns.txtcase create mode 100644 rust/validation/queries/ingest/time_s.txtcase create mode 100644 rust/validation/queries/ingest/time_us.txtcase create mode 100644 rust/validation/queries/ingest/timestamp_ms.txtcase create mode 100644 rust/validation/queries/ingest/timestamp_ns.txtcase create mode 100644 rust/validation/queries/ingest/timestamp_s.txtcase create mode 100644 rust/validation/queries/ingest/timestamp_us.txtcase create mode 100644 rust/validation/queries/ingest/timestamptz_ms.txtcase create mode 100644 rust/validation/queries/ingest/timestamptz_ns.txtcase create mode 100644 rust/validation/queries/ingest/timestamptz_s.txtcase create mode 100644 rust/validation/queries/ingest/timestamptz_us.txtcase create mode 100644 rust/validation/queries/type/bind/binary.toml create mode 100644 rust/validation/queries/type/bind/binary_view.toml create mode 100644 rust/validation/queries/type/bind/date.toml create mode 100644 rust/validation/queries/type/bind/decimal.toml create mode 100644 rust/validation/queries/type/bind/fixed_size_binary.toml create mode 100644 rust/validation/queries/type/bind/float16.txtcase create mode 100644 rust/validation/queries/type/bind/float32.schema.json create mode 100644 rust/validation/queries/type/bind/float64.bind.json create mode 100644 rust/validation/queries/type/bind/float64.json create mode 100644 rust/validation/queries/type/bind/int16.schema.json create mode 100644 rust/validation/queries/type/bind/int32.schema.json create mode 100644 rust/validation/queries/type/bind/large_binary.toml create mode 100644 rust/validation/queries/type/bind/string_view.toml create mode 100644 rust/validation/queries/type/bind/time_ms.txtcase create mode 100644 rust/validation/queries/type/bind/time_ns.txtcase create mode 100644 rust/validation/queries/type/bind/time_s.txtcase create mode 100644 rust/validation/queries/type/bind/time_us.txtcase create mode 100644 rust/validation/queries/type/bind/timestamp_ms.txtcase create mode 100644 rust/validation/queries/type/bind/timestamp_ns.txtcase create mode 100644 rust/validation/queries/type/bind/timestamp_s.txtcase create mode 100644 rust/validation/queries/type/bind/timestamp_us.txtcase create mode 100644 rust/validation/queries/type/bind/timestamptz_ms.txtcase create mode 100644 rust/validation/queries/type/bind/timestamptz_ns.txtcase create mode 100644 rust/validation/queries/type/bind/timestamptz_s.txtcase create mode 100644 rust/validation/queries/type/bind/timestamptz_us.txtcase create mode 100644 rust/validation/queries/type/literal/binary.sql create mode 100644 rust/validation/queries/type/literal/decimal.json create mode 100644 rust/validation/queries/type/literal/decimal.schema.json create mode 100644 rust/validation/queries/type/literal/float32.schema.json create mode 100644 rust/validation/queries/type/literal/int16.schema.json create mode 100644 rust/validation/queries/type/literal/int32.schema.json create mode 100644 rust/validation/queries/type/literal/time.json create mode 100644 rust/validation/queries/type/literal/time.schema.json create mode 100644 rust/validation/queries/type/literal/timestamp.sql create mode 100644 rust/validation/queries/type/literal/timestamptz.sql create mode 100644 rust/validation/queries/type/select/binary.setup.sql create mode 100644 rust/validation/queries/type/select/decimal.json create mode 100644 rust/validation/queries/type/select/decimal.schema.json create mode 100644 rust/validation/queries/type/select/float32.schema.json create mode 100644 rust/validation/queries/type/select/int16.schema.json create mode 100644 rust/validation/queries/type/select/int32.schema.json create mode 100644 rust/validation/queries/type/select/string.setup.sql create mode 100644 rust/validation/queries/type/select/time.json create mode 100644 rust/validation/queries/type/select/time.schema.json create mode 100644 rust/validation/queries/type/select/timestamp.setup.sql create mode 100644 rust/validation/queries/type/select/timestamptz.setup.sql create mode 100644 rust/validation/tests/.gitignore create mode 100644 rust/validation/tests/__init__.py create mode 100644 rust/validation/tests/conftest.py create mode 100644 rust/validation/tests/generate_documentation.py create mode 100644 rust/validation/tests/snowflake.py create mode 100644 rust/validation/tests/test_connection.py create mode 100644 rust/validation/tests/test_ingest.py create mode 100644 rust/validation/tests/test_query.py create mode 100644 rust/validation/tests/test_statement.py diff --git a/rust/validation/README.md b/rust/validation/README.md new file mode 100644 index 0000000..252bfe2 --- /dev/null +++ b/rust/validation/README.md @@ -0,0 +1,49 @@ + + +# Validation Suite Setup + +The following must be set up. + +- Snowflake Account and credentials +- A database, stored in `SNOWFLAKE_DATABASE` +- A schema (dataset), stored in `SNOWFLAKE_SCHEMA` + +## Authentication + +You must provide Snowflake credentials by setting the following environment variables: + +```bash +export SNOWFLAKE_URI="snowflake://user:password@account.snowflakecomputing.com/database/schema?warehouse=warehouse" +export SNOWFLAKE_DATABASE="your_database" +export SNOWFLAKE_SCHEMA="your_schema" +``` + +Example: +```bash +export SNOWFLAKE_URI="snowflake://testuser:mypassword@myorg-account1/testdb/public?warehouse=compute_wh&role=myrole" +export SNOWFLAKE_DATABASE="testdb" +export SNOWFLAKE_SCHEMA="public" +``` + +## Running Tests + +Once configured, run the validation suite with: + +```bash +cd rust +pixi run validate +``` diff --git a/rust/validation/pytest.ini b/rust/validation/pytest.ini new file mode 100644 index 0000000..7ee1426 --- /dev/null +++ b/rust/validation/pytest.ini @@ -0,0 +1,21 @@ +# Copyright (c) 2025 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +[pytest] +junit_suite_name = validation +junit_duration_report = call +xfail_strict = true + +markers = + feature: test for a driver-specific feature diff --git a/rust/validation/queries/ingest/binary.txtcase b/rust/validation/queries/ingest/binary.txtcase new file mode 100644 index 0000000..d8f7d03 --- /dev/null +++ b/rust/validation/queries/ingest/binary.txtcase @@ -0,0 +1,18 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// part: metadata + +[tags] +sql-type-name = "BINARY" diff --git a/rust/validation/queries/ingest/binary_view.toml b/rust/validation/queries/ingest/binary_view.toml new file mode 100644 index 0000000..9d1b344 --- /dev/null +++ b/rust/validation/queries/ingest/binary_view.toml @@ -0,0 +1,16 @@ +# Copyright (c) 2025 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Skip binary_view tests - not supported by Snowflake driver +skip = "BINARY_VIEW not supported by Snowflake driver" diff --git a/rust/validation/queries/ingest/decimal.input.json b/rust/validation/queries/ingest/decimal.input.json new file mode 100644 index 0000000..bd5930c --- /dev/null +++ b/rust/validation/queries/ingest/decimal.input.json @@ -0,0 +1,5 @@ +{"idx": 0, "value": 0.00} +{"idx": 1, "value": 123.45} +{"idx": 2, "value": -123.45} +{"idx": 3, "value": 9999999.99} +{"idx": 4, "value": -9999999.99} diff --git a/rust/validation/queries/ingest/decimal.input.schema.json b/rust/validation/queries/ingest/decimal.input.schema.json new file mode 100644 index 0000000..0b7a6ac --- /dev/null +++ b/rust/validation/queries/ingest/decimal.input.schema.json @@ -0,0 +1,15 @@ +{ + "format": "+s", + "children": [ + { + "name": "idx", + "format": "l", + "flags": ["nullable"] + }, + { + "name": "value", + "format": "g", + "flags": ["nullable"] + } + ] +} diff --git a/rust/validation/queries/ingest/fixed_size_binary.txtcase b/rust/validation/queries/ingest/fixed_size_binary.txtcase new file mode 100644 index 0000000..4579f8d --- /dev/null +++ b/rust/validation/queries/ingest/fixed_size_binary.txtcase @@ -0,0 +1,18 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// part: metadata + +[tags] +sql-type-name = "BINARY(n)" diff --git a/rust/validation/queries/ingest/float32.input.schema.json b/rust/validation/queries/ingest/float32.input.schema.json new file mode 100644 index 0000000..0b7a6ac --- /dev/null +++ b/rust/validation/queries/ingest/float32.input.schema.json @@ -0,0 +1,15 @@ +{ + "format": "+s", + "children": [ + { + "name": "idx", + "format": "l", + "flags": ["nullable"] + }, + { + "name": "value", + "format": "g", + "flags": ["nullable"] + } + ] +} diff --git a/rust/validation/queries/ingest/int16.input.schema.json b/rust/validation/queries/ingest/int16.input.schema.json new file mode 100644 index 0000000..f11d2f8 --- /dev/null +++ b/rust/validation/queries/ingest/int16.input.schema.json @@ -0,0 +1,15 @@ +{ + "format": "+s", + "children": [ + { + "name": "idx", + "format": "l", + "flags": ["nullable"] + }, + { + "name": "value", + "format": "l", + "flags": ["nullable"] + } + ] +} diff --git a/rust/validation/queries/ingest/int32.input.schema.json b/rust/validation/queries/ingest/int32.input.schema.json new file mode 100644 index 0000000..f11d2f8 --- /dev/null +++ b/rust/validation/queries/ingest/int32.input.schema.json @@ -0,0 +1,15 @@ +{ + "format": "+s", + "children": [ + { + "name": "idx", + "format": "l", + "flags": ["nullable"] + }, + { + "name": "value", + "format": "l", + "flags": ["nullable"] + } + ] +} diff --git a/rust/validation/queries/ingest/large_binary.txtcase b/rust/validation/queries/ingest/large_binary.txtcase new file mode 100644 index 0000000..d8f7d03 --- /dev/null +++ b/rust/validation/queries/ingest/large_binary.txtcase @@ -0,0 +1,18 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// part: metadata + +[tags] +sql-type-name = "BINARY" diff --git a/rust/validation/queries/ingest/large_string.txtcase b/rust/validation/queries/ingest/large_string.txtcase new file mode 100644 index 0000000..c290338 --- /dev/null +++ b/rust/validation/queries/ingest/large_string.txtcase @@ -0,0 +1,18 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// part: metadata + +[tags] +sql-type-name = "STRING" diff --git a/rust/validation/queries/ingest/string.txtcase b/rust/validation/queries/ingest/string.txtcase new file mode 100644 index 0000000..c290338 --- /dev/null +++ b/rust/validation/queries/ingest/string.txtcase @@ -0,0 +1,18 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// part: metadata + +[tags] +sql-type-name = "STRING" diff --git a/rust/validation/queries/ingest/string_view.toml b/rust/validation/queries/ingest/string_view.toml new file mode 100644 index 0000000..1d7693c --- /dev/null +++ b/rust/validation/queries/ingest/string_view.toml @@ -0,0 +1,16 @@ +# Copyright (c) 2025 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Skip string_view tests - not supported by Snowflake driver +skip = "string view not supported by Snowflake driver" diff --git a/rust/validation/queries/ingest/time_ms.txtcase b/rust/validation/queries/ingest/time_ms.txtcase new file mode 100644 index 0000000..eda3e18 --- /dev/null +++ b/rust/validation/queries/ingest/time_ms.txtcase @@ -0,0 +1,18 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// part: metadata + +[tags] +sql-type-name = "TIME(3)" diff --git a/rust/validation/queries/ingest/time_ns.txtcase b/rust/validation/queries/ingest/time_ns.txtcase new file mode 100644 index 0000000..c66d02b --- /dev/null +++ b/rust/validation/queries/ingest/time_ns.txtcase @@ -0,0 +1,18 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// part: metadata + +[tags] +sql-type-name = "TIME(9)" diff --git a/rust/validation/queries/ingest/time_s.txtcase b/rust/validation/queries/ingest/time_s.txtcase new file mode 100644 index 0000000..966300c --- /dev/null +++ b/rust/validation/queries/ingest/time_s.txtcase @@ -0,0 +1,18 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// part: metadata + +[tags] +sql-type-name = "TIME(0)" diff --git a/rust/validation/queries/ingest/time_us.txtcase b/rust/validation/queries/ingest/time_us.txtcase new file mode 100644 index 0000000..bfcfbcf --- /dev/null +++ b/rust/validation/queries/ingest/time_us.txtcase @@ -0,0 +1,18 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// part: metadata + +[tags] +sql-type-name = "TIME(6)" diff --git a/rust/validation/queries/ingest/timestamp_ms.txtcase b/rust/validation/queries/ingest/timestamp_ms.txtcase new file mode 100644 index 0000000..8df59ca --- /dev/null +++ b/rust/validation/queries/ingest/timestamp_ms.txtcase @@ -0,0 +1,18 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// part: metadata + +[tags] +sql-type-name = "TIMESTAMP_NTZ(3)" diff --git a/rust/validation/queries/ingest/timestamp_ns.txtcase b/rust/validation/queries/ingest/timestamp_ns.txtcase new file mode 100644 index 0000000..e86fe48 --- /dev/null +++ b/rust/validation/queries/ingest/timestamp_ns.txtcase @@ -0,0 +1,18 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// part: metadata + +[tags] +sql-type-name = "TIMESTAMP_NTZ(9)" diff --git a/rust/validation/queries/ingest/timestamp_s.txtcase b/rust/validation/queries/ingest/timestamp_s.txtcase new file mode 100644 index 0000000..df7ba91 --- /dev/null +++ b/rust/validation/queries/ingest/timestamp_s.txtcase @@ -0,0 +1,18 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// part: metadata + +[tags] +sql-type-name = "TIMESTAMP_NTZ(0)" diff --git a/rust/validation/queries/ingest/timestamp_us.txtcase b/rust/validation/queries/ingest/timestamp_us.txtcase new file mode 100644 index 0000000..2fb6cb0 --- /dev/null +++ b/rust/validation/queries/ingest/timestamp_us.txtcase @@ -0,0 +1,18 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// part: metadata + +[tags] +sql-type-name = "TIMESTAMP_NTZ(6)" diff --git a/rust/validation/queries/ingest/timestamptz_ms.txtcase b/rust/validation/queries/ingest/timestamptz_ms.txtcase new file mode 100644 index 0000000..aaa6b8a --- /dev/null +++ b/rust/validation/queries/ingest/timestamptz_ms.txtcase @@ -0,0 +1,18 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// part: metadata + +[tags] +sql-type-name = "TIMESTAMP_LTZ(3)" diff --git a/rust/validation/queries/ingest/timestamptz_ns.txtcase b/rust/validation/queries/ingest/timestamptz_ns.txtcase new file mode 100644 index 0000000..924df80 --- /dev/null +++ b/rust/validation/queries/ingest/timestamptz_ns.txtcase @@ -0,0 +1,18 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// part: metadata + +[tags] +sql-type-name = "TIMESTAMP_LTZ(9)" diff --git a/rust/validation/queries/ingest/timestamptz_s.txtcase b/rust/validation/queries/ingest/timestamptz_s.txtcase new file mode 100644 index 0000000..184631b --- /dev/null +++ b/rust/validation/queries/ingest/timestamptz_s.txtcase @@ -0,0 +1,18 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// part: metadata + +[tags] +sql-type-name = "TIMESTAMP_LTZ(0)" diff --git a/rust/validation/queries/ingest/timestamptz_us.txtcase b/rust/validation/queries/ingest/timestamptz_us.txtcase new file mode 100644 index 0000000..899313c --- /dev/null +++ b/rust/validation/queries/ingest/timestamptz_us.txtcase @@ -0,0 +1,18 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// part: metadata + +[tags] +sql-type-name = "TIMESTAMP_LTZ(6)" diff --git a/rust/validation/queries/type/bind/binary.toml b/rust/validation/queries/type/bind/binary.toml new file mode 100644 index 0000000..2a1d94f --- /dev/null +++ b/rust/validation/queries/type/bind/binary.toml @@ -0,0 +1,16 @@ +# Copyright (c) 2025 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Skip binary binding tests - not supported by Snowflake driver binding layer +skip = "Binary parameter binding not supported by Snowflake driver" diff --git a/rust/validation/queries/type/bind/binary_view.toml b/rust/validation/queries/type/bind/binary_view.toml new file mode 100644 index 0000000..097305b --- /dev/null +++ b/rust/validation/queries/type/bind/binary_view.toml @@ -0,0 +1,15 @@ +# Copyright (c) 2025 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +skip = "binary_view parameter binding not supported by Snowflake driver" diff --git a/rust/validation/queries/type/bind/date.toml b/rust/validation/queries/type/bind/date.toml new file mode 100644 index 0000000..f3325fb --- /dev/null +++ b/rust/validation/queries/type/bind/date.toml @@ -0,0 +1,16 @@ +# Copyright (c) 2025 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Skip date binding tests - not supported by Snowflake driver binding layer +skip = "Date parameter binding not supported by Snowflake driver" diff --git a/rust/validation/queries/type/bind/decimal.toml b/rust/validation/queries/type/bind/decimal.toml new file mode 100644 index 0000000..e920682 --- /dev/null +++ b/rust/validation/queries/type/bind/decimal.toml @@ -0,0 +1,15 @@ +# Copyright (c) 2025 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +skip = "decimal parameter binding not supported by Snowflake driver" diff --git a/rust/validation/queries/type/bind/fixed_size_binary.toml b/rust/validation/queries/type/bind/fixed_size_binary.toml new file mode 100644 index 0000000..cad0d1b --- /dev/null +++ b/rust/validation/queries/type/bind/fixed_size_binary.toml @@ -0,0 +1,15 @@ +# Copyright (c) 2025 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +skip = "fixed_size_binary parameter binding not supported by Snowflake driver" diff --git a/rust/validation/queries/type/bind/float16.txtcase b/rust/validation/queries/type/bind/float16.txtcase new file mode 100644 index 0000000..61690fc --- /dev/null +++ b/rust/validation/queries/type/bind/float16.txtcase @@ -0,0 +1,16 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// part: metadata +skip = "float16 parameter binding not supported by Snowflake driver" diff --git a/rust/validation/queries/type/bind/float32.schema.json b/rust/validation/queries/type/bind/float32.schema.json new file mode 100644 index 0000000..48ee373 --- /dev/null +++ b/rust/validation/queries/type/bind/float32.schema.json @@ -0,0 +1,10 @@ +{ + "format": "+s", + "children": [ + { + "name": "res", + "format": "g", + "flags": ["nullable"] + } + ] +} diff --git a/rust/validation/queries/type/bind/float64.bind.json b/rust/validation/queries/type/bind/float64.bind.json new file mode 100644 index 0000000..75775dd --- /dev/null +++ b/rust/validation/queries/type/bind/float64.bind.json @@ -0,0 +1,6 @@ +{"res": 3.141592653589793} +{"res": -2.5} +{"res": 0.0} +{"res": 1.797693e+38} +{"res": -1.797693e+38} +{"res": null} diff --git a/rust/validation/queries/type/bind/float64.json b/rust/validation/queries/type/bind/float64.json new file mode 100644 index 0000000..15ffcd5 --- /dev/null +++ b/rust/validation/queries/type/bind/float64.json @@ -0,0 +1,6 @@ +{"res": null} +{"res": -1.797693e+38} +{"res": -2.5} +{"res": 0.0} +{"res": 3.1415927} +{"res": 1.797693e+38} diff --git a/rust/validation/queries/type/bind/int16.schema.json b/rust/validation/queries/type/bind/int16.schema.json new file mode 100644 index 0000000..89e94b6 --- /dev/null +++ b/rust/validation/queries/type/bind/int16.schema.json @@ -0,0 +1,10 @@ +{ + "format": "+s", + "children": [ + { + "name": "res", + "format": "l", + "flags": ["nullable"] + } + ] +} diff --git a/rust/validation/queries/type/bind/int32.schema.json b/rust/validation/queries/type/bind/int32.schema.json new file mode 100644 index 0000000..89e94b6 --- /dev/null +++ b/rust/validation/queries/type/bind/int32.schema.json @@ -0,0 +1,10 @@ +{ + "format": "+s", + "children": [ + { + "name": "res", + "format": "l", + "flags": ["nullable"] + } + ] +} diff --git a/rust/validation/queries/type/bind/large_binary.toml b/rust/validation/queries/type/bind/large_binary.toml new file mode 100644 index 0000000..434a8b8 --- /dev/null +++ b/rust/validation/queries/type/bind/large_binary.toml @@ -0,0 +1,15 @@ +# Copyright (c) 2025 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +skip = "large_binary parameter binding not supported by Snowflake driver" diff --git a/rust/validation/queries/type/bind/string_view.toml b/rust/validation/queries/type/bind/string_view.toml new file mode 100644 index 0000000..069a563 --- /dev/null +++ b/rust/validation/queries/type/bind/string_view.toml @@ -0,0 +1,15 @@ +# Copyright (c) 2025 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +skip = "string_view parameter binding not supported by Snowflake driver" diff --git a/rust/validation/queries/type/bind/time_ms.txtcase b/rust/validation/queries/type/bind/time_ms.txtcase new file mode 100644 index 0000000..7b7b09d --- /dev/null +++ b/rust/validation/queries/type/bind/time_ms.txtcase @@ -0,0 +1,16 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// part: metadata +skip = "time_ms parameter binding not supported by Snowflake driver" diff --git a/rust/validation/queries/type/bind/time_ns.txtcase b/rust/validation/queries/type/bind/time_ns.txtcase new file mode 100644 index 0000000..e0b3e3d --- /dev/null +++ b/rust/validation/queries/type/bind/time_ns.txtcase @@ -0,0 +1,16 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// part: metadata +skip = "time_ns parameter binding not supported by Snowflake driver" diff --git a/rust/validation/queries/type/bind/time_s.txtcase b/rust/validation/queries/type/bind/time_s.txtcase new file mode 100644 index 0000000..5573ad8 --- /dev/null +++ b/rust/validation/queries/type/bind/time_s.txtcase @@ -0,0 +1,16 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// part: metadata +skip = "time_s parameter binding not supported by Snowflake driver" diff --git a/rust/validation/queries/type/bind/time_us.txtcase b/rust/validation/queries/type/bind/time_us.txtcase new file mode 100644 index 0000000..056d1e7 --- /dev/null +++ b/rust/validation/queries/type/bind/time_us.txtcase @@ -0,0 +1,16 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// part: metadata +skip = "time_us parameter binding not supported by Snowflake driver" diff --git a/rust/validation/queries/type/bind/timestamp_ms.txtcase b/rust/validation/queries/type/bind/timestamp_ms.txtcase new file mode 100644 index 0000000..897ea15 --- /dev/null +++ b/rust/validation/queries/type/bind/timestamp_ms.txtcase @@ -0,0 +1,16 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// part: metadata +skip = "timestamp_ms parameter binding not supported by Snowflake driver" diff --git a/rust/validation/queries/type/bind/timestamp_ns.txtcase b/rust/validation/queries/type/bind/timestamp_ns.txtcase new file mode 100644 index 0000000..81bc30f --- /dev/null +++ b/rust/validation/queries/type/bind/timestamp_ns.txtcase @@ -0,0 +1,16 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// part: metadata +skip = "timestamp_ns parameter binding not supported by Snowflake driver" diff --git a/rust/validation/queries/type/bind/timestamp_s.txtcase b/rust/validation/queries/type/bind/timestamp_s.txtcase new file mode 100644 index 0000000..07dbb68 --- /dev/null +++ b/rust/validation/queries/type/bind/timestamp_s.txtcase @@ -0,0 +1,16 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// part: metadata +skip = "timestamp_s parameter binding not supported by Snowflake driver" diff --git a/rust/validation/queries/type/bind/timestamp_us.txtcase b/rust/validation/queries/type/bind/timestamp_us.txtcase new file mode 100644 index 0000000..84f4d71 --- /dev/null +++ b/rust/validation/queries/type/bind/timestamp_us.txtcase @@ -0,0 +1,16 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// part: metadata +skip = "timestamp_us parameter binding not supported by Snowflake driver" diff --git a/rust/validation/queries/type/bind/timestamptz_ms.txtcase b/rust/validation/queries/type/bind/timestamptz_ms.txtcase new file mode 100644 index 0000000..468fbaa --- /dev/null +++ b/rust/validation/queries/type/bind/timestamptz_ms.txtcase @@ -0,0 +1,16 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// part: metadata +skip = "timestamptz_ms parameter binding not supported by Snowflake driver" diff --git a/rust/validation/queries/type/bind/timestamptz_ns.txtcase b/rust/validation/queries/type/bind/timestamptz_ns.txtcase new file mode 100644 index 0000000..71f8d1b --- /dev/null +++ b/rust/validation/queries/type/bind/timestamptz_ns.txtcase @@ -0,0 +1,16 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// part: metadata +skip = "timestamptz_ns parameter binding not supported by Snowflake driver" diff --git a/rust/validation/queries/type/bind/timestamptz_s.txtcase b/rust/validation/queries/type/bind/timestamptz_s.txtcase new file mode 100644 index 0000000..77696b4 --- /dev/null +++ b/rust/validation/queries/type/bind/timestamptz_s.txtcase @@ -0,0 +1,16 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// part: metadata +skip = "timestamptz_s parameter binding not supported by Snowflake driver" diff --git a/rust/validation/queries/type/bind/timestamptz_us.txtcase b/rust/validation/queries/type/bind/timestamptz_us.txtcase new file mode 100644 index 0000000..916beb7 --- /dev/null +++ b/rust/validation/queries/type/bind/timestamptz_us.txtcase @@ -0,0 +1,16 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// part: metadata +skip = "timestamptz_us parameter binding not supported by Snowflake driver" diff --git a/rust/validation/queries/type/literal/binary.sql b/rust/validation/queries/type/literal/binary.sql new file mode 100644 index 0000000..7e3bae5 --- /dev/null +++ b/rust/validation/queries/type/literal/binary.sql @@ -0,0 +1 @@ +SELECT X'e38193e38293e381abe381a1e381afe38081e4b896e7958cefbc81' AS "res" diff --git a/rust/validation/queries/type/literal/decimal.json b/rust/validation/queries/type/literal/decimal.json new file mode 100644 index 0000000..5a01358 --- /dev/null +++ b/rust/validation/queries/type/literal/decimal.json @@ -0,0 +1 @@ +{"res": 123.45} diff --git a/rust/validation/queries/type/literal/decimal.schema.json b/rust/validation/queries/type/literal/decimal.schema.json new file mode 100644 index 0000000..48ee373 --- /dev/null +++ b/rust/validation/queries/type/literal/decimal.schema.json @@ -0,0 +1,10 @@ +{ + "format": "+s", + "children": [ + { + "name": "res", + "format": "g", + "flags": ["nullable"] + } + ] +} diff --git a/rust/validation/queries/type/literal/float32.schema.json b/rust/validation/queries/type/literal/float32.schema.json new file mode 100644 index 0000000..48ee373 --- /dev/null +++ b/rust/validation/queries/type/literal/float32.schema.json @@ -0,0 +1,10 @@ +{ + "format": "+s", + "children": [ + { + "name": "res", + "format": "g", + "flags": ["nullable"] + } + ] +} diff --git a/rust/validation/queries/type/literal/int16.schema.json b/rust/validation/queries/type/literal/int16.schema.json new file mode 100644 index 0000000..89e94b6 --- /dev/null +++ b/rust/validation/queries/type/literal/int16.schema.json @@ -0,0 +1,10 @@ +{ + "format": "+s", + "children": [ + { + "name": "res", + "format": "l", + "flags": ["nullable"] + } + ] +} diff --git a/rust/validation/queries/type/literal/int32.schema.json b/rust/validation/queries/type/literal/int32.schema.json new file mode 100644 index 0000000..89e94b6 --- /dev/null +++ b/rust/validation/queries/type/literal/int32.schema.json @@ -0,0 +1,10 @@ +{ + "format": "+s", + "children": [ + { + "name": "res", + "format": "l", + "flags": ["nullable"] + } + ] +} diff --git a/rust/validation/queries/type/literal/time.json b/rust/validation/queries/type/literal/time.json new file mode 100644 index 0000000..1833ba5 --- /dev/null +++ b/rust/validation/queries/type/literal/time.json @@ -0,0 +1 @@ +{"res": 49531123456000} diff --git a/rust/validation/queries/type/literal/time.schema.json b/rust/validation/queries/type/literal/time.schema.json new file mode 100644 index 0000000..8cd1a21 --- /dev/null +++ b/rust/validation/queries/type/literal/time.schema.json @@ -0,0 +1,10 @@ +{ + "format": "+s", + "children": [ + { + "name": "res", + "format": "ttn", + "flags": ["nullable"] + } + ] +} diff --git a/rust/validation/queries/type/literal/timestamp.sql b/rust/validation/queries/type/literal/timestamp.sql new file mode 100644 index 0000000..5991af9 --- /dev/null +++ b/rust/validation/queries/type/literal/timestamp.sql @@ -0,0 +1 @@ +SELECT TIMESTAMP '2023-05-15 13:45:30'::TIMESTAMP(6) AS res diff --git a/rust/validation/queries/type/literal/timestamptz.sql b/rust/validation/queries/type/literal/timestamptz.sql new file mode 100644 index 0000000..70a8c70 --- /dev/null +++ b/rust/validation/queries/type/literal/timestamptz.sql @@ -0,0 +1 @@ +SELECT '2023-05-15 13:45:30+00:00'::TIMESTAMP_TZ(6) AS res diff --git a/rust/validation/queries/type/select/binary.setup.sql b/rust/validation/queries/type/select/binary.setup.sql new file mode 100644 index 0000000..ba89c9f --- /dev/null +++ b/rust/validation/queries/type/select/binary.setup.sql @@ -0,0 +1,10 @@ +CREATE TABLE test_binary ( + idx INTEGER, + res VARBINARY(1000) +); + +INSERT INTO test_binary (idx, res) VALUES (1, X'e38193e38293e381abe381a1e381afe38081e4b896e7958cefbc81'); -- 'こんにちは、世界!' in UTF-8 +INSERT INTO test_binary (idx, res) VALUES (2, X'00'); +INSERT INTO test_binary (idx, res) VALUES (3, X'deadbeef'); +INSERT INTO test_binary (idx, res) VALUES (4, X''); +INSERT INTO test_binary (idx, res) VALUES (5, NULL); diff --git a/rust/validation/queries/type/select/decimal.json b/rust/validation/queries/type/select/decimal.json new file mode 100644 index 0000000..371aae5 --- /dev/null +++ b/rust/validation/queries/type/select/decimal.json @@ -0,0 +1,5 @@ +{"res": 123.45} +{"res": 0.00} +{"res": -999.99} +{"res": 9999999.99} +{"res": null} diff --git a/rust/validation/queries/type/select/decimal.schema.json b/rust/validation/queries/type/select/decimal.schema.json new file mode 100644 index 0000000..48ee373 --- /dev/null +++ b/rust/validation/queries/type/select/decimal.schema.json @@ -0,0 +1,10 @@ +{ + "format": "+s", + "children": [ + { + "name": "res", + "format": "g", + "flags": ["nullable"] + } + ] +} diff --git a/rust/validation/queries/type/select/float32.schema.json b/rust/validation/queries/type/select/float32.schema.json new file mode 100644 index 0000000..48ee373 --- /dev/null +++ b/rust/validation/queries/type/select/float32.schema.json @@ -0,0 +1,10 @@ +{ + "format": "+s", + "children": [ + { + "name": "res", + "format": "g", + "flags": ["nullable"] + } + ] +} diff --git a/rust/validation/queries/type/select/int16.schema.json b/rust/validation/queries/type/select/int16.schema.json new file mode 100644 index 0000000..89e94b6 --- /dev/null +++ b/rust/validation/queries/type/select/int16.schema.json @@ -0,0 +1,10 @@ +{ + "format": "+s", + "children": [ + { + "name": "res", + "format": "l", + "flags": ["nullable"] + } + ] +} diff --git a/rust/validation/queries/type/select/int32.schema.json b/rust/validation/queries/type/select/int32.schema.json new file mode 100644 index 0000000..89e94b6 --- /dev/null +++ b/rust/validation/queries/type/select/int32.schema.json @@ -0,0 +1,10 @@ +{ + "format": "+s", + "children": [ + { + "name": "res", + "format": "l", + "flags": ["nullable"] + } + ] +} diff --git a/rust/validation/queries/type/select/string.setup.sql b/rust/validation/queries/type/select/string.setup.sql new file mode 100644 index 0000000..d096207 --- /dev/null +++ b/rust/validation/queries/type/select/string.setup.sql @@ -0,0 +1,10 @@ +CREATE TABLE test_string ( + idx INTEGER, + res VARCHAR(1000) +); + +INSERT INTO test_string ("idx", "res") VALUES (1, 'hello'); +INSERT INTO test_string ("idx", "res") VALUES (2, ''); +INSERT INTO test_string ("idx", "res") VALUES (3, 'Special chars: !@#$%^&*()_+{}|:"<>?~`-=[]\\;'',./'); +INSERT INTO test_string ("idx", "res") VALUES (4, 'Unicode: 你好, Привет, こんにちは, สวัสดี'); +INSERT INTO test_string ("idx", "res") VALUES (5, NULL); diff --git a/rust/validation/queries/type/select/time.json b/rust/validation/queries/type/select/time.json new file mode 100644 index 0000000..b881096 --- /dev/null +++ b/rust/validation/queries/type/select/time.json @@ -0,0 +1,5 @@ +{"res": 49531123456000} +{"res": 0} +{"res": 86399999999000} +{"res": 45045500000000} +{"res": null} diff --git a/rust/validation/queries/type/select/time.schema.json b/rust/validation/queries/type/select/time.schema.json new file mode 100644 index 0000000..8cd1a21 --- /dev/null +++ b/rust/validation/queries/type/select/time.schema.json @@ -0,0 +1,10 @@ +{ + "format": "+s", + "children": [ + { + "name": "res", + "format": "ttn", + "flags": ["nullable"] + } + ] +} diff --git a/rust/validation/queries/type/select/timestamp.setup.sql b/rust/validation/queries/type/select/timestamp.setup.sql new file mode 100644 index 0000000..cc07255 --- /dev/null +++ b/rust/validation/queries/type/select/timestamp.setup.sql @@ -0,0 +1,10 @@ +CREATE TABLE test_timestamp ( + idx INTEGER, + res TIMESTAMP(6) +); + +INSERT INTO test_timestamp (idx, res) VALUES (1, TIMESTAMP '2023-05-15 13:45:30'); +INSERT INTO test_timestamp (idx, res) VALUES (2, TIMESTAMP '2000-01-01 00:00:00'); +INSERT INTO test_timestamp (idx, res) VALUES (3, TIMESTAMP '1969-07-20 20:17:40'); +INSERT INTO test_timestamp (idx, res) VALUES (4, TIMESTAMP '9999-12-31 23:59:59'); +INSERT INTO test_timestamp (idx, res) VALUES (5, NULL); diff --git a/rust/validation/queries/type/select/timestamptz.setup.sql b/rust/validation/queries/type/select/timestamptz.setup.sql new file mode 100644 index 0000000..4aa9412 --- /dev/null +++ b/rust/validation/queries/type/select/timestamptz.setup.sql @@ -0,0 +1,10 @@ +CREATE TABLE test_timestamptz ( + idx INTEGER, + res TIMESTAMP_TZ(6) +); + +INSERT INTO test_timestamptz (idx, res) VALUES (1, '2023-05-15 13:45:30+00:00'::TIMESTAMP_TZ); +INSERT INTO test_timestamptz (idx, res) VALUES (2, '2000-01-01 00:00:00+00:00'::TIMESTAMP_TZ); +INSERT INTO test_timestamptz (idx, res) VALUES (3, '1969-07-20 20:17:40+00:00'::TIMESTAMP_TZ); +INSERT INTO test_timestamptz (idx, res) VALUES (4, '9999-12-31 23:59:59+00:00'::TIMESTAMP_TZ); +INSERT INTO test_timestamptz (idx, res) VALUES (5, NULL); diff --git a/rust/validation/tests/.gitignore b/rust/validation/tests/.gitignore new file mode 100644 index 0000000..0b1d246 --- /dev/null +++ b/rust/validation/tests/.gitignore @@ -0,0 +1,13 @@ +# Copyright (c) 2025 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/rust/validation/tests/__init__.py b/rust/validation/tests/__init__.py new file mode 100644 index 0000000..2940c8a --- /dev/null +++ b/rust/validation/tests/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2025 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Snowflake ADBC driver validation tests.""" diff --git a/rust/validation/tests/conftest.py b/rust/validation/tests/conftest.py new file mode 100644 index 0000000..03bd06c --- /dev/null +++ b/rust/validation/tests/conftest.py @@ -0,0 +1,52 @@ +# Copyright (c) 2025 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys +from pathlib import Path + +import adbc_drivers_validation.model +import adbc_drivers_validation.tests.conftest +import pytest +from adbc_drivers_validation.tests.conftest import ( # noqa: F401 + conn, + conn_factory, + manual_test, + pytest_collection_modifyitems, +) + +from . import snowflake + + +def pytest_addoption(parser): + adbc_drivers_validation.tests.conftest.pytest_addoption(parser) + parser.addoption("--vendor-version", action="store", default="10") + + +@pytest.fixture(scope="session") +def driver(request, pytestconfig) -> adbc_drivers_validation.model.DriverQuirks: + driver = request.param + assert driver.startswith("snowflake") + return snowflake.get_quirks(pytestconfig.getoption("vendor_version")) + + +@pytest.fixture(scope="session") +def driver_path(driver: adbc_drivers_validation.model.DriverQuirks) -> str: + ext = { + "win32": "dll", + "darwin": "dylib", + }.get(sys.platform, "so") + return str( + Path(__file__).parent.parent.parent + / f"build/libadbc_driver_{driver.name}.{ext}" + ) diff --git a/rust/validation/tests/generate_documentation.py b/rust/validation/tests/generate_documentation.py new file mode 100644 index 0000000..b083b41 --- /dev/null +++ b/rust/validation/tests/generate_documentation.py @@ -0,0 +1,36 @@ +# Copyright (c) 2025 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import argparse +from pathlib import Path + +import adbc_drivers_validation.generate_documentation as generate_documentation + +from . import snowflake + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + + template = Path(__file__).parent.parent.parent / "docs/snowflake.md" + template = template.resolve() + + reports = [report.resolve() for report in Path(".").glob("validation-report*.xml")] + generate_documentation.generate( + snowflake.get_quirks, + reports, + template, + args.output.resolve(), + ) diff --git a/rust/validation/tests/snowflake.py b/rust/validation/tests/snowflake.py new file mode 100644 index 0000000..ee87106 --- /dev/null +++ b/rust/validation/tests/snowflake.py @@ -0,0 +1,94 @@ +# Copyright (c) 2025 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import functools +import re +from pathlib import Path + +from adbc_drivers_validation import model, quirks + + +class SnowflakeQuirks(model.DriverQuirks): + name = "snowflake" + driver = "adbc_driver_snowflake" + driver_name = "ADBC Driver Foundry Driver for Snowflake" + vendor_name = "Snowflake" + vendor_version = re.compile(r"10\.[0-9]+\.[0-9]+") + short_version = "10" + features = model.DriverFeatures( + connection_get_table_schema=True, + connection_transactions=True, + get_objects=True, + get_objects_constraints_foreign=False, + get_objects_constraints_primary=False, + get_objects_constraints_unique=False, + statement_bind=True, + statement_bulk_ingest=True, + statement_bulk_ingest_catalog=True, + statement_bulk_ingest_schema=True, + statement_bulk_ingest_temporary=False, + statement_execute_schema=True, + statement_get_parameter_schema=False, + statement_prepare=True, + statement_rows_affected=True, + statement_rows_affected_ddl=False, + current_catalog=model.FromEnv("SNOWFLAKE_DATABASE"), + current_schema=model.FromEnv("SNOWFLAKE_SCHEMA"), + secondary_schema=model.FromEnv("SNOWFLAKE_SCHEMA_SECONDARY"), + secondary_catalog=model.FromEnv("SNOWFLAKE_DATABASE_SECONDARY"), + secondary_catalog_schema=model.FromEnv("SNOWFLAKE_DATABASE_SECONDARY_SCHEMA"), + supported_xdbc_fields=[], + ) + setup = model.DriverSetup( + database={ + "uri": model.FromEnv("SNOWFLAKE_URI"), + "adbc.snowflake.sql.client_option.use_high_precision": "false", + "timezone": "UTC", + }, + connection={}, + statement={}, + ) + + @property + def queries_paths(self) -> tuple[Path]: + return (Path(__file__).parent.parent / "queries",) + + def is_table_not_found(self, table_name: str | None, error: Exception) -> bool: + error_msg = str(error).lower() + + # Snowflake returns "Object does not exist, or operation cannot be performed." + # Error codes 002043 or 002003 for table/object not found errors + # Snowflake doesn't include the table name in the error message + return ( + "002043" in error_msg + or "002003" in error_msg + or "object does not exist" in error_msg + or "does not exist or not authorized" in error_msg + ) + + def quote_one_identifier(self, identifier: str) -> str: + """Quote an identifier to preserve case and ensure consistency.""" + identifier = identifier.replace('"', '""') + return f'"{identifier}"' + + def split_statement(self, statement: str) -> list[str]: + return quirks.split_statement(statement, dialect=self.name) + + +@functools.cache +def get_quirks(version: str) -> SnowflakeQuirks: + quirks = SnowflakeQuirks() + if version != quirks.short_version: + raise ValueError(f"Unsupported Snowflake version: {version}") + return quirks diff --git a/rust/validation/tests/test_connection.py b/rust/validation/tests/test_connection.py new file mode 100644 index 0000000..1a66e50 --- /dev/null +++ b/rust/validation/tests/test_connection.py @@ -0,0 +1,25 @@ +# Copyright (c) 2025 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from adbc_drivers_validation.tests.connection import ( + TestConnection, # noqa: F401 + generate_tests, +) + +from . import snowflake + + +def pytest_generate_tests(metafunc) -> None: + quirks = [snowflake.get_quirks(metafunc.config.getoption("vendor_version"))] + return generate_tests(quirks, metafunc) diff --git a/rust/validation/tests/test_ingest.py b/rust/validation/tests/test_ingest.py new file mode 100644 index 0000000..f57427e --- /dev/null +++ b/rust/validation/tests/test_ingest.py @@ -0,0 +1,27 @@ +# Copyright (c) 2025 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import adbc_drivers_validation.tests.ingest as ingest_tests + +from . import snowflake + + +def pytest_generate_tests(metafunc) -> None: + quirks = [snowflake.get_quirks(metafunc.config.getoption("vendor_version"))] + return ingest_tests.generate_tests(quirks, metafunc) + + +class TestIngest(ingest_tests.TestIngest): + pass diff --git a/rust/validation/tests/test_query.py b/rust/validation/tests/test_query.py new file mode 100644 index 0000000..3352e0e --- /dev/null +++ b/rust/validation/tests/test_query.py @@ -0,0 +1,113 @@ +# Copyright (c) 2025 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import functools +import re +from pathlib import Path + +import adbc_drivers_validation.model as model +import adbc_drivers_validation.tests.query as query_tests + +from . import snowflake + +# Store the original query function +_original_query = model.query + + +@functools.cache +def _snowflake_query(path: Path) -> str: + """ + Snowflake-specific query function that quotes table names and column names in SQL. + + This is required because snowflake turns the table and column names uppercase if not quoted. + """ + sql = _original_query(path) + + # Pattern to match table names like test_boolean, test_int32, etc. + table_pattern = r"\b(test_\w+)\b" + + def quote_identifier(match): + identifier = match.group(1) + return f'"{identifier}"' + + # Replace unquoted table references with quoted ones + quoted_sql = re.sub(table_pattern, quote_identifier, sql) + + # Quote column names in CREATE TABLE statements + # Pattern to find CREATE TABLE (...) and quote unquoted column names inside + def quote_create_table_columns(match): + before_paren = match.group(1) # "CREATE TABLE table_name (" + columns_part = match.group(2) # column definitions + after_paren = match.group(3) # ");" + + # Quote common column names like idx, res, etc. + column_names = r"\b(idx|res|value|name)\b(?=\s)" + quoted_columns = re.sub(column_names, lambda m: f'"{m.group(1)}"', columns_part) + + return before_paren + quoted_columns + after_paren + + # Apply column quoting to CREATE TABLE statements + create_table_pattern = r"(CREATE\s+TABLE\s+[^(]+\s*\()(.*?)(\);?)" + quoted_sql = re.sub( + create_table_pattern, + quote_create_table_columns, + quoted_sql, + flags=re.DOTALL | re.IGNORECASE, + ) + + # Quote column names in SELECT statements + column_pattern = r"\b(res\w*|idx\w*)\b(?=\s|$|,)" + quoted_sql = re.sub(column_pattern, quote_identifier, quoted_sql) + + # Quote column names in INSERT statements + # Pattern to match INSERT INTO table_name (columns) VALUES + def quote_insert_columns(match): + before_cols = match.group(1) # "INSERT INTO table_name (" + columns_list = match.group(2) # column list + after_cols = match.group(3) # ") VALUES" + + # Split by comma and quote each unquoted column name + columns = [col.strip() for col in columns_list.split(",")] + quoted_columns = [] + + for col in columns: + # If already quoted, keep as is; otherwise quote it + if col.startswith('"') and col.endswith('"'): + quoted_columns.append(col) + else: + quoted_columns.append(f'"{col}"') + + return before_cols + ", ".join(quoted_columns) + after_cols + + # Apply column quoting to INSERT statements + insert_pattern = r"(INSERT\s+INTO\s+[^(]+\s*\()([^)]+)(\)\s+VALUES)" + quoted_sql = re.sub( + insert_pattern, quote_insert_columns, quoted_sql, flags=re.IGNORECASE + ) + + return quoted_sql + + +# Monkey patch the query function for Snowflake tests +model.query = _snowflake_query + + +def pytest_generate_tests(metafunc) -> None: + quirks = [snowflake.get_quirks(metafunc.config.getoption("vendor_version"))] + return query_tests.generate_tests(quirks, metafunc) + + +class TestQuery(query_tests.TestQuery): + pass diff --git a/rust/validation/tests/test_statement.py b/rust/validation/tests/test_statement.py new file mode 100644 index 0000000..ad2a160 --- /dev/null +++ b/rust/validation/tests/test_statement.py @@ -0,0 +1,27 @@ +# Copyright (c) 2025 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import adbc_drivers_validation.tests.statement as statement_tests + +from . import snowflake + + +def pytest_generate_tests(metafunc) -> None: + quirks = [snowflake.get_quirks(metafunc.config.getoption("vendor_version"))] + return statement_tests.generate_tests(quirks, metafunc) + + +class TestStatement(statement_tests.TestStatement): + pass From 48b8bf5a30d6d2eadf3b74c1feb56761b78d336c Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Fri, 20 Mar 2026 14:01:22 -0400 Subject: [PATCH 31/76] rename package --- rust/Cargo.lock | 109 +++++++++++++++++++------------------- rust/Cargo.toml | 8 +-- rust/tests/integration.rs | 4 +- 3 files changed, 61 insertions(+), 60 deletions(-) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 5ee183a..9d88442 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -3,7 +3,7 @@ version = 4 [[package]] -name = "adbc-snowflake" +name = "adbc-driver-snowflake" version = "0.1.0" dependencies = [ "adbc_core", @@ -496,9 +496,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "aws-config" -version = "1.8.13" +version = "1.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c456581cb3c77fafcc8c67204a70680d40b61112d6da78c77bd31d945b65f1b5" +checksum = "11493b0bad143270fb8ad284a096dd529ba91924c5409adeac856cc1bf047dbc" dependencies = [ "aws-credential-types", "aws-runtime", @@ -516,7 +516,7 @@ dependencies = [ "fastrand", "hex", "http 1.4.0", - "ring", + "sha1", "time", "tokio", "tracing", @@ -526,9 +526,9 @@ dependencies = [ [[package]] name = "aws-credential-types" -version = "1.2.11" +version = "1.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cd362783681b15d136480ad555a099e82ecd8e2d10a841e14dfd0078d67fee3" +checksum = "8f20799b373a1be121fe3005fba0c2090af9411573878f224df44b42727fcaf7" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", @@ -538,9 +538,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.16.1" +version = "1.16.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94bffc006df10ac2a68c83692d734a465f8ee6c5b384d8545a636f81d858f4bf" +checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" dependencies = [ "aws-lc-sys", "untrusted 0.7.1", @@ -549,9 +549,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.38.0" +version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4321e568ed89bb5a7d291a7f37997c2c0df89809d7b6d12062c81ddb54aa782e" +checksum = "1fa7e52a4c5c547c741610a2c6f123f3881e409b714cd27e6798ef020c514f0a" dependencies = [ "cc", "cmake", @@ -561,9 +561,9 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.6.0" +version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c635c2dc792cb4a11ce1a4f392a925340d1bdf499289b5ec1ec6810954eb43f5" +checksum = "5fc0651c57e384202e47153c1260b84a9936e19803d747615edf199dc3b98d17" dependencies = [ "aws-credential-types", "aws-sigv4", @@ -575,6 +575,7 @@ dependencies = [ "aws-smithy-types", "aws-types", "bytes", + "bytes-utils", "fastrand", "http 0.2.12", "http 1.4.0", @@ -588,9 +589,9 @@ dependencies = [ [[package]] name = "aws-sdk-s3" -version = "1.122.0" +version = "1.127.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94c2ca0cba97e8e279eb6c0b2d0aa10db5959000e602ab2b7c02de6b85d4c19b" +checksum = "151783f64e0dcddeb4965d08e36c276b4400a46caa88805a2e36d497deaf031a" dependencies = [ "aws-credential-types", "aws-runtime", @@ -623,9 +624,9 @@ dependencies = [ [[package]] name = "aws-sdk-sso" -version = "1.93.0" +version = "1.97.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dcb38bb33fc0a11f1ffc3e3e85669e0a11a37690b86f77e75306d8f369146a0" +checksum = "9aadc669e184501caaa6beafb28c6267fc1baef0810fb58f9b205485ca3f2567" dependencies = [ "aws-credential-types", "aws-runtime", @@ -647,9 +648,9 @@ dependencies = [ [[package]] name = "aws-sdk-ssooidc" -version = "1.95.0" +version = "1.99.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ada8ffbea7bd1be1f53df1dadb0f8fdb04badb13185b3321b929d1ee3caad09" +checksum = "1342a7db8f358d3de0aed2007a0b54e875458e39848d54cc1d46700b2bfcb0a8" dependencies = [ "aws-credential-types", "aws-runtime", @@ -671,9 +672,9 @@ dependencies = [ [[package]] name = "aws-sdk-sts" -version = "1.97.0" +version = "1.101.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6443ccadc777095d5ed13e21f5c364878c9f5bad4e35187a6cdbd863b0afcad" +checksum = "ab41ad64e4051ecabeea802d6a17845a91e83287e1dd249e6963ea1ba78c428a" dependencies = [ "aws-credential-types", "aws-runtime", @@ -696,9 +697,9 @@ dependencies = [ [[package]] name = "aws-sigv4" -version = "1.3.8" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efa49f3c607b92daae0c078d48a4571f599f966dce3caee5f1ea55c4d9073f99" +checksum = "b0b660013a6683ab23797778e21f1f854744fdf05f68204b4cca4c8c04b5d1f4" dependencies = [ "aws-credential-types", "aws-smithy-eventstream", @@ -724,9 +725,9 @@ dependencies = [ [[package]] name = "aws-smithy-async" -version = "1.2.11" +version = "1.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52eec3db979d18cb807fc1070961cc51d87d069abe9ab57917769687368a8c6c" +checksum = "2ffcaf626bdda484571968400c326a244598634dc75fd451325a54ad1a59acfc" dependencies = [ "futures-util", "pin-project-lite", @@ -735,9 +736,9 @@ dependencies = [ [[package]] name = "aws-smithy-checksums" -version = "0.64.3" +version = "0.64.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddcf418858f9f3edd228acb8759d77394fed7531cce78d02bdda499025368439" +checksum = "6750f3dd509b0694a4377f0293ed2f9630d710b1cebe281fa8bac8f099f88bc6" dependencies = [ "aws-smithy-http", "aws-smithy-types", @@ -756,9 +757,9 @@ dependencies = [ [[package]] name = "aws-smithy-eventstream" -version = "0.60.18" +version = "0.60.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35b9c7354a3b13c66f60fe4616d6d1969c9fd36b1b5333a5dfb3ee716b33c588" +checksum = "faf09d74e5e32f76b8762da505a3cd59303e367a664ca67295387baa8c1d7548" dependencies = [ "aws-smithy-types", "bytes", @@ -767,9 +768,9 @@ dependencies = [ [[package]] name = "aws-smithy-http" -version = "0.63.3" +version = "0.63.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630e67f2a31094ffa51b210ae030855cb8f3b7ee1329bdd8d085aaf61e8b97fc" +checksum = "ba1ab2dc1c2c3749ead27180d333c42f11be8b0e934058fb4b2258ee8dbe5231" dependencies = [ "aws-smithy-eventstream", "aws-smithy-runtime-api", @@ -789,9 +790,9 @@ dependencies = [ [[package]] name = "aws-smithy-http-client" -version = "1.1.9" +version = "1.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12fb0abf49ff0cab20fd31ac1215ed7ce0ea92286ba09e2854b42ba5cabe7525" +checksum = "6a2f165a7feee6f263028b899d0a181987f4fa7179a6411a32a439fba7c5f769" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", @@ -819,27 +820,27 @@ dependencies = [ [[package]] name = "aws-smithy-json" -version = "0.62.3" +version = "0.62.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cb96aa208d62ee94104645f7b2ecaf77bf27edf161590b6224bfbac2832f979" +checksum = "9648b0bb82a2eedd844052c6ad2a1a822d1f8e3adee5fbf668366717e428856a" dependencies = [ "aws-smithy-types", ] [[package]] name = "aws-smithy-observability" -version = "0.2.4" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0a46543fbc94621080b3cf553eb4cbbdc41dd9780a30c4756400f0139440a1d" +checksum = "a06c2315d173edbf1920da8ba3a7189695827002e4c0fc961973ab1c54abca9c" dependencies = [ "aws-smithy-runtime-api", ] [[package]] name = "aws-smithy-query" -version = "0.60.13" +version = "0.60.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cebbddb6f3a5bd81553643e9c7daf3cc3dc5b0b5f398ac668630e8a84e6fff0" +checksum = "1a56d79744fb3edb5d722ef79d86081e121d3b9422cb209eb03aea6aa4f21ebd" dependencies = [ "aws-smithy-types", "urlencoding", @@ -847,9 +848,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.10.0" +version = "1.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3df87c14f0127a0d77eb261c3bc45d5b4833e2a1f63583ebfb728e4852134ee" +checksum = "028999056d2d2fd58a697232f9eec4a643cf73a71cf327690a7edad1d2af2110" dependencies = [ "aws-smithy-async", "aws-smithy-http", @@ -872,9 +873,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.11.3" +version = "1.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49952c52f7eebb72ce2a754d3866cc0f87b97d2a46146b79f80f3a93fb2b3716" +checksum = "876ab3c9c29791ba4ba02b780a3049e21ec63dabda09268b175272c3733a79e6" dependencies = [ "aws-smithy-async", "aws-smithy-types", @@ -889,9 +890,9 @@ dependencies = [ [[package]] name = "aws-smithy-types" -version = "1.4.3" +version = "1.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3a26048eeab0ddeba4b4f9d51654c79af8c3b32357dc5f336cee85ab331c33" +checksum = "9d73dbfbaa8e4bc57b9045137680b958d274823509a360abfd8e1d514d40c95c" dependencies = [ "base64-simd", "bytes", @@ -915,18 +916,18 @@ dependencies = [ [[package]] name = "aws-smithy-xml" -version = "0.60.13" +version = "0.60.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11b2f670422ff42bf7065031e72b45bc52a3508bd089f743ea90731ca2b6ea57" +checksum = "0ce02add1aa3677d022f8adf81dcbe3046a95f17a1b1e8979c145cd21d3d22b3" dependencies = [ "xmlparser", ] [[package]] name = "aws-types" -version = "1.3.11" +version = "1.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d980627d2dd7bfc32a3c025685a033eeab8d365cc840c631ef59d1b8f428164" +checksum = "47c8323699dd9b3c8d5b3c13051ae9cdef58fd179957c882f8374dd8725962d9" dependencies = [ "aws-credential-types", "aws-smithy-async", @@ -2226,9 +2227,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" @@ -5099,18 +5100,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.46" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5030500cb2d66bdfbb4ebc9563be6ce7005a4b5d0f26be0c523870fe372ca6" +checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.46" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5f86989a046a79640b9d8867c823349a139367bda96549794fcc3313ce91f4e" +checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" dependencies = [ "proc-macro2", "quote", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index e9f2114..115d329 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -21,7 +21,7 @@ # under the License. [package] -name = "adbc-snowflake" +name = "adbc-driver-snowflake" version = "0.1.0" edition = "2024" @@ -32,7 +32,7 @@ crate-type = ["cdylib", "rlib"] adbc_core = "0.22.0" adbc_ffi = "0.22.0" sf_core = { git = "https://github.com/snowflakedb/universal-driver", subdirectory = "sf_core", rev = "080422e05fbd727f68d7c494e564ec625e1375d6" } -arrow-array = { version = ">=53.1.0, <59", default-features = false, features = ["ffi"] } -arrow-buffer = { version = ">=53.1.0, <59", default-features = false } -arrow-schema = { version = ">=53.1.0, <59", default-features = false } +arrow-array = { version = "57.3.0", default-features = false, features = ["ffi"] } +arrow-buffer = { version = "57.3.0", default-features = false } +arrow-schema = { version = "57.3.0", default-features = false } tokio = { version = "1", features = ["rt-multi-thread"] } diff --git a/rust/tests/integration.rs b/rust/tests/integration.rs index 95b7344..ad6f9ec 100644 --- a/rust/tests/integration.rs +++ b/rust/tests/integration.rs @@ -25,7 +25,7 @@ use adbc_core::{ Connection as _, Database as _, Driver as _, Optionable, Statement as _, options::{OptionConnection, OptionDatabase, OptionValue}, }; -use adbc_snowflake::{Database, Driver}; +use adbc_driver_snowflake::{Database, Driver}; use arrow_array::cast::AsArray; use arrow_schema::{DataType, TimeUnit}; @@ -58,7 +58,7 @@ fn make_db() -> Option { Some(db) } -fn make_connection() -> Option { +fn make_connection() -> Option { Some(make_db()?.new_connection().expect("new_connection")) } From 088a37ca93d0ccbbc440b80d4f3e97868fcb0bc4 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Sun, 22 Mar 2026 12:36:18 -0400 Subject: [PATCH 32/76] more features and integration tests --- rust/src/connection.rs | 55 ++- rust/src/database.rs | 53 +- rust/src/get_objects.rs | 763 +++++++++++++++++++++++++++++ rust/src/lib.rs | 2 + rust/src/statement.rs | 277 ++++++++++- rust/tests/integration.rs | 68 ++- rust/validation/tests/snowflake.py | 15 +- 7 files changed, 1161 insertions(+), 72 deletions(-) create mode 100644 rust/src/get_objects.rs diff --git a/rust/src/connection.rs b/rust/src/connection.rs index 5f744de..98a71c8 100644 --- a/rust/src/connection.rs +++ b/rust/src/connection.rs @@ -56,13 +56,13 @@ impl Drop for Connection { } } -struct SingleBatchReader { +pub(crate) struct SingleBatchReader { batch: Option, schema: std::sync::Arc, } impl SingleBatchReader { - fn new(batch: RecordBatch) -> Self { + pub(crate) fn new(batch: RecordBatch) -> Self { let schema = batch.schema(); Self { batch: Some(batch), @@ -256,10 +256,13 @@ impl adbc_core::Connection for Connection { conn_handle: self.conn_handle, query: None, target_table: None, + ingest_catalog: None, + ingest_schema: None, ingest_mode: None, query_tag: None, use_high_precision: self.use_high_precision, timestamp_precision: self.timestamp_precision, + bound_batches: vec![], }) } @@ -290,6 +293,7 @@ impl adbc_core::Connection for Connection { (InfoCode::DriverVersion, 0, 2), (InfoCode::DriverAdbcVersion, 2, 0), (InfoCode::VendorVersion, 0, 3), + (InfoCode::DriverArrowVersion, 0, 4), ]; let selected: Vec<_> = match &codes { @@ -316,6 +320,7 @@ impl adbc_core::Connection for Connection { "ADBC Snowflake Driver (Rust)", env!("CARGO_PKG_VERSION"), vendor_version.as_str(), + "v57.3.0", ])) as ArrayRef; let bool_values = Arc::new(BooleanArray::from(vec![true, false])) as ArrayRef; let int64_values = @@ -423,14 +428,22 @@ impl adbc_core::Connection for Connection { #[allow(refining_impl_trait)] fn get_objects( &self, - _depth: ObjectDepth, - _catalog: Option<&str>, - _db_schema: Option<&str>, - _table_name: Option<&str>, - _table_type: Option>, - _column_name: Option<&str>, + depth: ObjectDepth, + catalog: Option<&str>, + db_schema: Option<&str>, + table_name: Option<&str>, + table_type: Option>, + column_name: Option<&str>, ) -> Result> { - Err(crate::error::not_implemented("get_objects")) + crate::get_objects::execute_get_objects( + self, + &depth, + catalog, + db_schema, + table_name, + table_type, + column_name, + ) } fn get_table_schema( @@ -542,22 +555,30 @@ impl adbc_core::Connection for Connection { } fn commit(&mut self) -> Result<()> { + if self.autocommit { + return Err(Error::with_message_and_status( + "cannot commit: autocommit is enabled", + Status::InvalidState, + )); + } self.execute_simple("COMMIT")?; self.active_transaction = false; - if !self.autocommit { - self.execute_simple("BEGIN")?; - self.active_transaction = true; - } + self.execute_simple("BEGIN")?; + self.active_transaction = true; Ok(()) } fn rollback(&mut self) -> Result<()> { + if self.autocommit { + return Err(Error::with_message_and_status( + "cannot rollback: autocommit is enabled", + Status::InvalidState, + )); + } self.execute_simple("ROLLBACK")?; self.active_transaction = false; - if !self.autocommit { - self.execute_simple("BEGIN")?; - self.active_transaction = true; - } + self.execute_simple("BEGIN")?; + self.active_transaction = true; Ok(()) } diff --git a/rust/src/database.rs b/rust/src/database.rs index 266fe41..3e1ab63 100644 --- a/rust/src/database.rs +++ b/rust/src/database.rs @@ -90,9 +90,7 @@ fn adbc_db_opt_to_sf(key: &str, value: &OptionValue) -> Result { - "ocsp_fail_open_mode".to_string() - } + "adbc.snowflake.sql.client_option.ocsp_fail_open_mode" => "ocsp_fail_open_mode".to_string(), // Session behaviour "adbc.snowflake.sql.client_option.keep_session_alive" => "keep_session_alive".to_string(), "adbc.snowflake.sql.client_option.disable_telemetry" => "disable_telemetry".to_string(), @@ -197,8 +195,10 @@ impl Optionable for Database { param_names::VERIFY_CERTIFICATES.as_str().to_string(), verify.clone(), ); - self.sf_settings - .insert(param_names::VERIFY_HOSTNAME.as_str().to_string(), verify.clone()); + self.sf_settings.insert( + param_names::VERIFY_HOSTNAME.as_str().to_string(), + verify.clone(), + ); self.inner .runtime .block_on(async { @@ -226,10 +226,11 @@ impl Optionable for Database { // enabled (fail-open / advisory) → ADVISORY; disabled (strict) → ENABLED. if key_str == "adbc.snowflake.sql.client_option.ocsp_fail_open_mode" { let fail_open = matches!(&value, OptionValue::String(s) if s == "enabled"); - let mode = - Setting::String(if fail_open { "ADVISORY" } else { "ENABLED" }.to_string()); - self.sf_settings - .insert(param_names::CRL_CHECK_MODE.as_str().to_string(), mode.clone()); + let mode = Setting::String(if fail_open { "ADVISORY" } else { "ENABLED" }.to_string()); + self.sf_settings.insert( + param_names::CRL_CHECK_MODE.as_str().to_string(), + mode.clone(), + ); self.inner .runtime .block_on(self.inner.sf.database_set_option( @@ -601,7 +602,8 @@ mod tests { ); // Compound: verify_certificates and verify_hostname must be false assert_eq!( - db.sf_settings.get(param_names::VERIFY_CERTIFICATES.as_str()), + db.sf_settings + .get(param_names::VERIFY_CERTIFICATES.as_str()), Some(&Setting::Bool(false)) ); assert_eq!( @@ -633,7 +635,8 @@ mod tests { "disabled" ); assert_eq!( - db.sf_settings.get(param_names::VERIFY_CERTIFICATES.as_str()), + db.sf_settings + .get(param_names::VERIFY_CERTIFICATES.as_str()), Some(&Setting::Bool(true)) ); assert_eq!( @@ -647,9 +650,7 @@ mod tests { use sf_core::config::settings::Setting; let mut db = make_db(); db.set_option( - OptionDatabase::Other( - "adbc.snowflake.sql.client_option.ocsp_fail_open_mode".into(), - ), + OptionDatabase::Other("adbc.snowflake.sql.client_option.ocsp_fail_open_mode".into()), OptionValue::String("enabled".into()), ) .unwrap(); @@ -671,9 +672,7 @@ mod tests { use sf_core::config::settings::Setting; let mut db = make_db(); db.set_option( - OptionDatabase::Other( - "adbc.snowflake.sql.client_option.ocsp_fail_open_mode".into(), - ), + OptionDatabase::Other("adbc.snowflake.sql.client_option.ocsp_fail_open_mode".into()), OptionValue::String("disabled".into()), ) .unwrap(); @@ -687,18 +686,9 @@ mod tests { fn simple_option_round_trips() { let mut db = make_db(); let cases = [ - ( - "adbc.snowflake.sql.region", - "us-east-1", - ), - ( - "adbc.snowflake.sql.client_option.login_timeout", - "30s", - ), - ( - "adbc.snowflake.sql.client_option.request_timeout", - "60s", - ), + ("adbc.snowflake.sql.region", "us-east-1"), + ("adbc.snowflake.sql.client_option.login_timeout", "30s"), + ("adbc.snowflake.sql.client_option.request_timeout", "60s"), ( "adbc.snowflake.sql.client_option.keep_session_alive", "enabled", @@ -707,10 +697,7 @@ mod tests { "adbc.snowflake.sql.client_option.disable_telemetry", "enabled", ), - ( - "adbc.snowflake.sql.client_option.tracing", - "debug", - ), + ("adbc.snowflake.sql.client_option.tracing", "debug"), ( "adbc.snowflake.sql.client_option.config_file", "/home/user/.snowflake/config.toml", diff --git a/rust/src/get_objects.rs b/rust/src/get_objects.rs new file mode 100644 index 0000000..cbbef18 --- /dev/null +++ b/rust/src/get_objects.rs @@ -0,0 +1,763 @@ +// Copyright (c) 2026 ADBC Drivers Contributors +// +// This file has been modified from its original version, which is +// under the Apache License: +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +// src/get_objects.rs + +use std::sync::Arc; + +use adbc_core::{ + error::{Error, Result, Status}, + options::ObjectDepth, + schemas, +}; +use arrow_array::{ + Array, ArrayRef, BooleanArray, Int16Array, Int32Array, ListArray, RecordBatch, + RecordBatchReader, StringArray, StructArray, +}; +use arrow_buffer::{NullBuffer, OffsetBuffer, ScalarBuffer}; +use arrow_schema::{DataType, Field, Fields}; + +use crate::connection::{Connection, SingleBatchReader}; + +// ── SQL helpers ─────────────────────────────────────────────────────────────── + +fn sql_esc(s: &str) -> String { + s.replace('\'', "''") +} + +/// Converts an optional filter into a SQL ILIKE pattern. None → match-all. +fn ilike(p: Option<&str>) -> String { + match p { + None | Some("") | Some("%") | Some(".*") => "%".to_string(), + Some(s) => sql_esc(s), + } +} + +/// Returns an `information_schema.` prefix, optionally qualified by a specific +/// catalog when the filter is an exact name (not a wildcard). +fn info_prefix(catalog_filter: Option<&str>) -> String { + match catalog_filter { + Some(c) if !c.is_empty() && !c.contains('%') && !c.contains('_') => { + format!("\"{}\".information_schema.", sql_esc(c)) + } + _ => "information_schema.".to_string(), + } +} + +/// Builds a table-type WHERE fragment, mapping ADBC 'TABLE' → Snowflake 'BASE TABLE'. +fn type_clause(filter: &Option>) -> String { + let Some(types) = filter else { + return String::new(); + }; + if types.is_empty() { + return String::new(); + } + let quoted: Vec = types + .iter() + .map(|t| { + let up = t.to_uppercase(); + let sf = if up == "TABLE" { "BASE TABLE" } else { t }; + format!("'{}'", sql_esc(sf)) + }) + .collect(); + format!(" AND table_type IN ({})", quoted.join(", ")) +} + +// ── query execution ─────────────────────────────────────────────────────────── + +/// Runs a SQL query and returns all rows as `Vec>>`. +fn run_query(conn: &Connection, sql: &str) -> Result>>> { + let stmt = conn + .inner + .sf + .statement_new(conn.conn_handle) + .map_err(crate::error::api_error_to_adbc_error)?; + + let result = conn.inner.runtime.block_on(async { + conn.inner + .sf + .statement_set_sql_query(stmt, sql.to_string()) + .await?; + conn.inner.sf.statement_execute_query(stmt, None).await + }); + let _ = conn.inner.sf.statement_release(stmt); + let exec = result.map_err(crate::error::api_error_to_adbc_error)?; + + let raw = Box::into_raw(exec.stream) as *mut arrow_array::ffi_stream::FFI_ArrowArrayStream; + let reader = unsafe { arrow_array::ffi_stream::ArrowArrayStreamReader::from_raw(raw) } + .map_err(|e| Error::with_message_and_status(e.to_string(), Status::IO))?; + + let mut rows = Vec::new(); + for batch_res in reader { + let batch = + batch_res.map_err(|e| Error::with_message_and_status(e.to_string(), Status::IO))?; + let ncols = batch.num_columns(); + for row_idx in 0..batch.num_rows() { + let row = (0..ncols) + .map(|c| cell_to_string(batch.column(c).as_ref(), row_idx)) + .collect(); + rows.push(row); + } + } + Ok(rows) +} + +fn cell_to_string(arr: &dyn Array, i: usize) -> Option { + if arr.is_null(i) { + return None; + } + if let Some(s) = arr.as_any().downcast_ref::() { + return Some(s.value(i).to_string()); + } + if let Some(n) = arr.as_any().downcast_ref::() { + return Some(n.value(i).to_string()); + } + if let Some(n) = arr.as_any().downcast_ref::() { + return Some(n.value(i).to_string()); + } + None +} + +// ── data holders ───────────────────────────────────────────────────────────── + +struct ColEntry { + name: String, + ordinal_position: i32, + remarks: Option, + xdbc_type_name: Option, + xdbc_column_size: Option, + xdbc_char_octet_length: Option, + xdbc_decimal_digits: Option, + xdbc_num_prec_radix: Option, + xdbc_nullable: Option, + xdbc_is_nullable: Option, + xdbc_datetime_sub: Option, +} + +struct TableEntry { + name: String, + table_type: String, + columns: Vec, +} + +struct SchemaEntry { + name: String, + tables: Vec, +} + +struct CatalogEntry { + name: String, + schemas: Vec, +} + +// ── data collection ─────────────────────────────────────────────────────────── + +pub(crate) fn execute_get_objects( + conn: &Connection, + depth: &ObjectDepth, + catalog_filter: Option<&str>, + db_schema_filter: Option<&str>, + table_name_filter: Option<&str>, + table_type_filter: Option>, + column_name_filter: Option<&str>, +) -> Result> { + let entries = collect( + conn, + depth, + catalog_filter, + db_schema_filter, + table_name_filter, + table_type_filter, + column_name_filter, + )?; + let batch = build_batch(&entries, &depth)?; + Ok(Box::new(SingleBatchReader::new(batch))) +} + +fn collect( + conn: &Connection, + depth: &ObjectDepth, + catalog_filter: Option<&str>, + db_schema_filter: Option<&str>, + table_name_filter: Option<&str>, + table_type_filter: Option>, + column_name_filter: Option<&str>, +) -> Result> { + let cat_pat = ilike(catalog_filter); + let sch_pat = ilike(db_schema_filter); + let tbl_pat = ilike(table_name_filter); + let col_pat = ilike(column_name_filter); + + // 1. Catalogs — always from information_schema.databases (account-wide) + let cat_rows = run_query( + conn, + &format!( + "SELECT database_name FROM information_schema.databases \ + WHERE database_name ILIKE '{}' ORDER BY database_name", + cat_pat + ), + )?; + let catalog_names: Vec = cat_rows + .into_iter() + .filter_map(|r| r.into_iter().next().flatten()) + .collect(); + + if catalog_names.is_empty() || matches!(depth, ObjectDepth::Catalogs) { + return Ok(catalog_names + .into_iter() + .map(|name| CatalogEntry { + name, + schemas: vec![], + }) + .collect()); + } + + // 2. Schemas — scoped to the catalog(s) in view + // When catalog_filter is a specific database we qualify the prefix; otherwise + // information_schema resolves to the current database. + let prefix = info_prefix(catalog_filter); + let sch_rows = run_query( + conn, + &format!( + "SELECT catalog_name, schema_name FROM {}schemata \ + WHERE catalog_name ILIKE '{}' AND schema_name ILIKE '{}' \ + ORDER BY catalog_name, schema_name", + prefix, cat_pat, sch_pat + ), + )?; + + // Group schemas by catalog + let mut schemas_by_cat: std::collections::BTreeMap> = + catalog_names.iter().map(|n| (n.clone(), vec![])).collect(); + for row in &sch_rows { + if row.len() < 2 { + continue; + } + if let (Some(cat), Some(sch)) = (&row[0], &row[1]) { + schemas_by_cat + .entry(cat.clone()) + .or_default() + .push(sch.clone()); + } + } + + if matches!(depth, ObjectDepth::Schemas) { + return Ok(catalog_names + .into_iter() + .map(|cat| { + let schemas = schemas_by_cat + .remove(&cat) + .unwrap_or_default() + .into_iter() + .map(|sch| SchemaEntry { + name: sch, + tables: vec![], + }) + .collect(); + CatalogEntry { name: cat, schemas } + }) + .collect()); + } + + // 3. Tables + let tc = type_clause(&table_type_filter); + let tbl_rows = run_query( + conn, + &format!( + "SELECT table_catalog, table_schema, table_name, \ + CASE table_type WHEN 'BASE TABLE' THEN 'TABLE' ELSE table_type END \ + FROM {}tables \ + WHERE table_catalog ILIKE '{}' AND table_schema ILIKE '{}' \ + AND table_name ILIKE '{}'{} \ + ORDER BY table_catalog, table_schema, table_name", + prefix, cat_pat, sch_pat, tbl_pat, tc + ), + )?; + + // Group tables by (catalog, schema) + let mut tables_by_key: std::collections::BTreeMap<(String, String), Vec> = + std::collections::BTreeMap::new(); + for row in &tbl_rows { + if row.len() < 4 { + continue; + } + if let (Some(cat), Some(sch), Some(tbl), Some(tt)) = (&row[0], &row[1], &row[2], &row[3]) { + tables_by_key + .entry((cat.clone(), sch.clone())) + .or_default() + .push(TableEntry { + name: tbl.clone(), + table_type: tt.clone(), + columns: vec![], + }); + } + } + + if matches!(depth, ObjectDepth::Tables) { + return Ok(assemble(catalog_names, schemas_by_cat, tables_by_key)); + } + + // 4. Columns (All / Columns depth) + let col_rows = run_query( + conn, + &format!( + "SELECT table_catalog, table_schema, table_name, column_name, \ + ordinal_position::INTEGER, comment, data_type, is_nullable, \ + COALESCE(character_maximum_length, numeric_precision)::INTEGER, \ + character_octet_length::INTEGER, numeric_scale::INTEGER, \ + numeric_precision_radix::INTEGER, datetime_precision::INTEGER \ + FROM {}columns \ + WHERE table_catalog ILIKE '{}' AND table_schema ILIKE '{}' \ + AND table_name ILIKE '{}' AND column_name ILIKE '{}' \ + ORDER BY table_catalog, table_schema, table_name, ordinal_position", + prefix, cat_pat, sch_pat, tbl_pat, col_pat + ), + )?; + + for row in &col_rows { + if row.len() < 13 { + continue; + } + let (Some(cat), Some(sch), Some(tbl), Some(col_name)) = + (&row[0], &row[1], &row[2], &row[3]) + else { + continue; + }; + let key = (cat.clone(), sch.clone()); + if let Some(tables) = tables_by_key.get_mut(&key) { + if let Some(table) = tables.iter_mut().find(|t| &t.name == tbl) { + let nullable_str = row[7].clone(); + let nullable_int = nullable_str.as_deref().map(|s| { + if s.eq_ignore_ascii_case("YES") { + 1i16 + } else { + 0 + } + }); + let ordinal = row[4] + .as_deref() + .and_then(|s| s.parse::().ok()) + .unwrap_or(0); + table.columns.push(ColEntry { + name: col_name.clone(), + ordinal_position: ordinal, + remarks: row[5].clone(), + xdbc_type_name: row[6].clone(), + xdbc_column_size: row[8].as_deref().and_then(|s| s.parse().ok()), + xdbc_char_octet_length: row[9].as_deref().and_then(|s| s.parse().ok()), + xdbc_decimal_digits: row[10].as_deref().and_then(|s| s.parse().ok()), + xdbc_num_prec_radix: row[11].as_deref().and_then(|s| s.parse().ok()), + xdbc_nullable: nullable_int, + xdbc_is_nullable: nullable_str, + xdbc_datetime_sub: row[12].as_deref().and_then(|s| s.parse().ok()), + }); + } + } + } + + Ok(assemble(catalog_names, schemas_by_cat, tables_by_key)) +} + +fn assemble( + catalog_names: Vec, + mut schemas_by_cat: std::collections::BTreeMap>, + mut tables_by_key: std::collections::BTreeMap<(String, String), Vec>, +) -> Vec { + catalog_names + .into_iter() + .map(|cat| { + let schema_names = schemas_by_cat.remove(&cat).unwrap_or_default(); + let schemas = schema_names + .into_iter() + .map(|sch| { + let tables = tables_by_key + .remove(&(cat.clone(), sch.clone())) + .unwrap_or_default(); + SchemaEntry { name: sch, tables } + }) + .collect(); + CatalogEntry { name: cat, schemas } + }) + .collect() +} + +// ── Arrow output construction ───────────────────────────────────────────────── + +fn build_batch(entries: &[CatalogEntry], depth: &ObjectDepth) -> Result { + let schemas_null = matches!(depth, ObjectDepth::Catalogs); + let tables_null = matches!(depth, ObjectDepth::Catalogs | ObjectDepth::Schemas); + let cols_null = !matches!(depth, ObjectDepth::All | ObjectDepth::Columns); + + // Flatten counts + let n_cats = entries.len(); + let n_schemas: usize = entries.iter().map(|c| c.schemas.len()).sum(); + let n_tables: usize = entries + .iter() + .flat_map(|c| &c.schemas) + .map(|s| s.tables.len()) + .sum(); + let n_cols: usize = entries + .iter() + .flat_map(|c| &c.schemas) + .flat_map(|s| &s.tables) + .map(|t| t.columns.len()) + .sum(); + + // ── columns ─────────────────────────────────────────────────────────────── + let all_cols: Vec<&ColEntry> = entries + .iter() + .flat_map(|c| &c.schemas) + .flat_map(|s| &s.tables) + .flat_map(|t| &t.columns) + .collect(); + let col_struct = build_col_struct(&all_cols); + + // col offsets per table + let (col_offsets, col_null_buf) = if cols_null { + ( + vec![0i32; n_tables + 1], + Some(NullBuffer::new_null(n_tables)), + ) + } else { + let mut offs = Vec::with_capacity(n_tables + 1); + offs.push(0i32); + for c in entries + .iter() + .flat_map(|c| &c.schemas) + .flat_map(|s| &s.tables) + { + let last = *offs.last().unwrap(); + offs.push(last + c.columns.len() as i32); + } + (offs, None) + }; + let col_item_field = Arc::new(item_field_of(&schemas::COLUMN_SCHEMA)); + let col_list = make_list( + col_item_field, + col_offsets, + Arc::new(col_struct), + col_null_buf, + )?; + + // ── table_constraints (always null in our impl) ─────────────────────────── + let constraint_item_field = Arc::new(item_field_of(&schemas::CONSTRAINT_SCHEMA)); + let constraint_list = make_all_null_list(constraint_item_field, n_tables)?; + + // ── table struct ────────────────────────────────────────────────────────── + let tbl_struct = build_table_struct(entries, col_list, constraint_list)?; + + // table offsets per schema + let (tbl_offsets, tbl_null_buf) = if tables_null { + ( + vec![0i32; n_schemas + 1], + Some(NullBuffer::new_null(n_schemas)), + ) + } else { + let mut offs = Vec::with_capacity(n_schemas + 1); + offs.push(0i32); + for s in entries.iter().flat_map(|c| &c.schemas) { + let last = *offs.last().unwrap(); + offs.push(last + s.tables.len() as i32); + } + (offs, None) + }; + let tbl_item_field = Arc::new(item_field_of(&schemas::TABLE_SCHEMA)); + let tbl_list = make_list( + tbl_item_field, + tbl_offsets, + Arc::new(tbl_struct), + tbl_null_buf, + )?; + + // ── schema struct ───────────────────────────────────────────────────────── + let sch_struct = build_schema_struct(entries, tbl_list)?; + + // schema offsets per catalog + let (sch_offsets, sch_null_buf) = if schemas_null { + (vec![0i32; n_cats + 1], Some(NullBuffer::new_null(n_cats))) + } else { + let mut offs = Vec::with_capacity(n_cats + 1); + offs.push(0i32); + for c in entries { + let last = *offs.last().unwrap(); + offs.push(last + c.schemas.len() as i32); + } + (offs, None) + }; + let sch_item_field = Arc::new(item_field_of(&schemas::OBJECTS_DB_SCHEMA_SCHEMA)); + let sch_list = make_list( + sch_item_field, + sch_offsets, + Arc::new(sch_struct), + sch_null_buf, + )?; + + // ── top-level batch ─────────────────────────────────────────────────────── + let cat_names: Vec> = entries.iter().map(|c| Some(c.name.as_str())).collect(); + RecordBatch::try_new( + schemas::GET_OBJECTS_SCHEMA.clone(), + vec![ + Arc::new(StringArray::from(cat_names)) as ArrayRef, + Arc::new(sch_list) as ArrayRef, + ], + ) + .map_err(|e| Error::with_message_and_status(e.to_string(), Status::Internal)) +} + +// ── struct array builders ───────────────────────────────────────────────────── + +fn build_col_struct(cols: &[&ColEntry]) -> StructArray { + let n = cols.len(); + let mut col_name = StringBuilder::with_capacity(n, n * 16); + let mut ordinal = Int32Builder::with_capacity(n); + let mut remarks = StringBuilder::with_capacity(n, n * 8); + let mut xdbc_data_type = Int16Builder::with_capacity(n); + let mut xdbc_type_name = StringBuilder::with_capacity(n, n * 8); + let mut xdbc_col_size = Int32Builder::with_capacity(n); + let mut xdbc_dec_digits = Int16Builder::with_capacity(n); + let mut xdbc_radix = Int16Builder::with_capacity(n); + let mut xdbc_nullable = Int16Builder::with_capacity(n); + let mut xdbc_col_def = StringBuilder::with_capacity(n, 0); + let mut xdbc_sql_dt = Int16Builder::with_capacity(n); + let mut xdbc_dt_sub = Int16Builder::with_capacity(n); + let mut xdbc_octet_len = Int32Builder::with_capacity(n); + let mut xdbc_is_nullable = StringBuilder::with_capacity(n, n * 4); + let mut xdbc_scope_cat = StringBuilder::with_capacity(n, 0); + let mut xdbc_scope_sch = StringBuilder::with_capacity(n, 0); + let mut xdbc_scope_tbl = StringBuilder::with_capacity(n, 0); + let mut xdbc_auto_inc = BooleanBuilder::with_capacity(n); + let mut xdbc_gen_col = BooleanBuilder::with_capacity(n); + + for c in cols { + col_name.append_value(&c.name); + ordinal.append_value(c.ordinal_position); + append_opt_str(&mut remarks, c.remarks.as_deref()); + xdbc_data_type.append_null(); // computed from Arrow type — not available here + append_opt_str(&mut xdbc_type_name, c.xdbc_type_name.as_deref()); + append_opt_i32(&mut xdbc_col_size, c.xdbc_column_size); + append_opt_i16(&mut xdbc_dec_digits, c.xdbc_decimal_digits); + append_opt_i16(&mut xdbc_radix, c.xdbc_num_prec_radix); + append_opt_i16(&mut xdbc_nullable, c.xdbc_nullable); + xdbc_col_def.append_null(); + xdbc_sql_dt.append_null(); + append_opt_i16(&mut xdbc_dt_sub, c.xdbc_datetime_sub); + append_opt_i32(&mut xdbc_octet_len, c.xdbc_char_octet_length); + append_opt_str(&mut xdbc_is_nullable, c.xdbc_is_nullable.as_deref()); + xdbc_scope_cat.append_null(); + xdbc_scope_sch.append_null(); + xdbc_scope_tbl.append_null(); + xdbc_auto_inc.append_null(); + xdbc_gen_col.append_null(); + } + + let fields = struct_fields(&schemas::COLUMN_SCHEMA); + StructArray::try_new( + fields, + vec![ + Arc::new(col_name.finish()) as ArrayRef, + Arc::new(ordinal.finish()), + Arc::new(remarks.finish()), + Arc::new(xdbc_data_type.finish()), + Arc::new(xdbc_type_name.finish()), + Arc::new(xdbc_col_size.finish()), + Arc::new(xdbc_dec_digits.finish()), + Arc::new(xdbc_radix.finish()), + Arc::new(xdbc_nullable.finish()), + Arc::new(xdbc_col_def.finish()), + Arc::new(xdbc_sql_dt.finish()), + Arc::new(xdbc_dt_sub.finish()), + Arc::new(xdbc_octet_len.finish()), + Arc::new(xdbc_is_nullable.finish()), + Arc::new(xdbc_scope_cat.finish()), + Arc::new(xdbc_scope_sch.finish()), + Arc::new(xdbc_scope_tbl.finish()), + Arc::new(xdbc_auto_inc.finish()), + Arc::new(xdbc_gen_col.finish()), + ], + None, + ) + .expect("column StructArray construction") +} + +fn build_table_struct( + entries: &[CatalogEntry], + col_list: ListArray, + constraint_list: ListArray, +) -> Result { + let n_tables: usize = entries + .iter() + .flat_map(|c| &c.schemas) + .map(|s| s.tables.len()) + .sum(); + + let mut tbl_name = StringBuilder::with_capacity(n_tables, n_tables * 16); + let mut tbl_type = StringBuilder::with_capacity(n_tables, n_tables * 8); + + for t in entries + .iter() + .flat_map(|c| &c.schemas) + .flat_map(|s| &s.tables) + { + tbl_name.append_value(&t.name); + tbl_type.append_value(&t.table_type); + } + + let fields = struct_fields(&schemas::TABLE_SCHEMA); + StructArray::try_new( + fields, + vec![ + Arc::new(tbl_name.finish()) as ArrayRef, + Arc::new(tbl_type.finish()), + Arc::new(col_list), + Arc::new(constraint_list), + ], + None, + ) + .map_err(|e| Error::with_message_and_status(e.to_string(), Status::Internal)) +} + +fn build_schema_struct(entries: &[CatalogEntry], tbl_list: ListArray) -> Result { + let n_schemas: usize = entries.iter().map(|c| c.schemas.len()).sum(); + let mut sch_name = StringBuilder::with_capacity(n_schemas, n_schemas * 16); + + for s in entries.iter().flat_map(|c| &c.schemas) { + sch_name.append_value(&s.name); + } + + let fields = struct_fields(&schemas::OBJECTS_DB_SCHEMA_SCHEMA); + StructArray::try_new( + fields, + vec![Arc::new(sch_name.finish()) as ArrayRef, Arc::new(tbl_list)], + None, + ) + .map_err(|e| Error::with_message_and_status(e.to_string(), Status::Internal)) +} + +// ── list array helpers ──────────────────────────────────────────────────────── + +fn make_list( + item_field: Arc, + offsets: Vec, + values: ArrayRef, + null_buf: Option, +) -> Result { + let offs = OffsetBuffer::new(ScalarBuffer::from(offsets)); + ListArray::try_new(item_field, offs, values, null_buf) + .map_err(|e| Error::with_message_and_status(e.to_string(), Status::Internal)) +} + +/// Creates a ListArray where every entry is null (for table_constraints). +fn make_all_null_list(item_field: Arc, n: usize) -> Result { + let null_buf = if n > 0 { + Some(NullBuffer::new_null(n)) + } else { + None + }; + let offsets = vec![0i32; n + 1]; + // Empty values StructArray matching the item field's struct type + let values: ArrayRef = match item_field.data_type() { + DataType::Struct(fields) => { + let empty_arrays: Vec = fields + .iter() + .map(|f| empty_array_for(f.data_type())) + .collect(); + Arc::new( + StructArray::try_new(fields.clone(), empty_arrays, None) + .map_err(|e| Error::with_message_and_status(e.to_string(), Status::Internal))?, + ) + } + _ => { + return Err(Error::with_message_and_status( + "expected struct item type", + Status::Internal, + )); + } + }; + make_list(item_field, offsets, values, null_buf) +} + +// ── type extraction helpers ─────────────────────────────────────────────────── + +/// Extracts the `Fields` from a DataType::Struct stored in an adbc_core schema static. +fn struct_fields(dt: &DataType) -> Fields { + match dt { + DataType::Struct(f) => f.clone(), + _ => panic!("expected DataType::Struct"), + } +} + +/// Returns a `Field { name: "item", data_type: , nullable: true }` +/// that matches what `DataType::new_list(dt, nullable)` produces internally. +fn item_field_of(dt: &DataType) -> Field { + Field::new("item", dt.clone(), true) +} + +/// Returns an empty (zero-length) array for the given DataType, used when +/// building all-null list arrays that still need a correctly-typed values array. +fn empty_array_for(dt: &DataType) -> ArrayRef { + match dt { + DataType::Utf8 => Arc::new(StringArray::from(Vec::>::new())), + DataType::Int16 => Arc::new(Int16Array::from(Vec::>::new())), + DataType::Int32 => Arc::new(Int32Array::from(Vec::>::new())), + DataType::Boolean => Arc::new(BooleanArray::from(Vec::>::new())), + DataType::Struct(fields) => { + let children: Vec = fields + .iter() + .map(|f| empty_array_for(f.data_type())) + .collect(); + Arc::new( + StructArray::try_new(fields.clone(), children, None).expect("empty struct array"), + ) + } + DataType::List(f) => { + let values = empty_array_for(f.data_type()); + let offs = OffsetBuffer::::new(ScalarBuffer::from(vec![0i32])); + Arc::new(ListArray::try_new(f.clone(), offs, values, None).expect("empty list array")) + } + _ => Arc::new(StringArray::from(Vec::>::new())), + } +} + +// ── builder shorthand ───────────────────────────────────────────────────────── + +use arrow_array::builder::{BooleanBuilder, Int16Builder, Int32Builder, StringBuilder}; + +fn append_opt_str(b: &mut StringBuilder, v: Option<&str>) { + match v { + Some(s) => b.append_value(s), + None => b.append_null(), + } +} + +fn append_opt_i16(b: &mut Int16Builder, v: Option) { + match v { + Some(n) => b.append_value(n), + None => b.append_null(), + } +} + +fn append_opt_i32(b: &mut Int32Builder, v: Option) { + match v { + Some(n) => b.append_value(n), + None => b.append_null(), + } +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 17827ab..4dd972d 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -32,6 +32,8 @@ pub use database::Database; mod connection; pub use connection::Connection; +mod get_objects; + mod statement; pub use statement::Statement; diff --git a/rust/src/statement.rs b/rust/src/statement.rs index 402fd15..4d518d2 100644 --- a/rust/src/statement.rs +++ b/rust/src/statement.rs @@ -28,8 +28,8 @@ use adbc_core::{ error::{Error, Result, Status}, options::{OptionStatement, OptionValue}, }; -use arrow_array::{RecordBatch, RecordBatchReader}; -use arrow_schema::Schema; +use arrow_array::{Array, RecordBatch, RecordBatchReader}; +use arrow_schema::{DataType, Schema}; use sf_core::apis::database_driver_v1::Handle; use crate::driver::{Inner, TimestampPrecision}; @@ -40,10 +40,14 @@ pub struct Statement { pub(crate) conn_handle: Handle, pub(crate) query: Option, pub(crate) target_table: Option, + pub(crate) ingest_catalog: Option, + pub(crate) ingest_schema: Option, pub(crate) ingest_mode: Option, pub(crate) query_tag: Option, pub(crate) use_high_precision: bool, pub(crate) timestamp_precision: TimestampPrecision, + /// Parameter batches stored by bind() / bind_stream(). Each row is one execution. + pub(crate) bound_batches: Vec, } impl Drop for Statement { @@ -102,6 +106,22 @@ impl Optionable for Statement { } Ok(()) } + OptionStatement::Temporary => { + // Accepted silently; used to select CREATE TEMPORARY TABLE during ingest. + Ok(()) + } + OptionStatement::TargetCatalog => { + if let OptionValue::String(s) = value { + self.ingest_catalog = Some(s); + } + Ok(()) + } + OptionStatement::TargetDbSchema => { + if let OptionValue::String(s) = value { + self.ingest_schema = Some(s); + } + Ok(()) + } OptionStatement::Other(ref k) if k == "adbc.snowflake.statement.query_tag" => { if let OptionValue::String(s) = value { self.query_tag = Some(s); @@ -155,6 +175,64 @@ impl Optionable for Statement { } impl Statement { + /// Execute a parameterized query once per row of every bound batch. + /// Parameter values are substituted directly as SQL literals — this avoids + /// relying on sf_core's JSON binding path and works with all Snowflake + /// server versions without session configuration. + fn execute_bound( + &self, + query: String, + ) -> Result> { + let mut all_batches: Vec = Vec::new(); + let mut result_schema: Option> = None; + + for bound_batch in &self.bound_batches { + for row_idx in 0..bound_batch.num_rows() { + let bound_sql = substitute_params(&query, bound_batch, row_idx)?; + + let result = self + .inner + .runtime + .block_on(async { + self.inner + .sf + .statement_set_sql_query(self.stmt_handle, bound_sql) + .await?; + self.inner + .sf + .statement_execute_query(self.stmt_handle, None) + .await + }) + .map_err(crate::error::api_error_to_adbc_error)?; + + // Safety: same as execute(). + let raw = Box::into_raw(result.stream) + as *mut arrow_array::ffi_stream::FFI_ArrowArrayStream; + let reader = + unsafe { arrow_array::ffi_stream::ArrowArrayStreamReader::from_raw(raw) } + .map_err(|e| { + Error::with_message_and_status(e.to_string(), Status::IO) + })?; + + if result_schema.is_none() { + result_schema = Some(reader.schema()); + } + for batch in reader { + let batch = batch.map_err(|e| { + Error::with_message_and_status(e.to_string(), Status::IO) + })?; + all_batches.push(batch); + } + } + } + + let schema = result_schema.unwrap_or_else(|| Arc::new(Schema::empty())); + Ok(Box::new(ConcatReader { + batches: all_batches.into_iter(), + schema, + })) + } + fn apply_query_tag(&self) -> Result<()> { if let Some(ref tag) = self.query_tag { let escaped = tag.replace('\'', "''"); @@ -182,12 +260,20 @@ impl Statement { } impl adbc_core::Statement for Statement { - fn bind(&mut self, _batch: RecordBatch) -> Result<()> { - Err(crate::error::not_implemented("bind")) + fn bind(&mut self, batch: RecordBatch) -> Result<()> { + self.bound_batches = vec![batch]; + Ok(()) } - fn bind_stream(&mut self, _reader: Box) -> Result<()> { - Err(crate::error::not_implemented("bind_stream")) + fn bind_stream(&mut self, reader: Box) -> Result<()> { + let mut batches = Vec::new(); + for batch_result in reader { + let batch = batch_result + .map_err(|e| Error::with_message_and_status(e.to_string(), Status::IO))?; + batches.push(batch); + } + self.bound_batches = batches; + Ok(()) } #[allow(refining_impl_trait)] @@ -203,6 +289,11 @@ impl adbc_core::Statement for Statement { self.apply_query_tag()?; + // If parameters are bound, execute once per row and concatenate results. + if !self.bound_batches.is_empty() { + return self.execute_bound(query); + } + let result = self .inner .runtime @@ -254,11 +345,48 @@ impl adbc_core::Statement for Statement { }) .map_err(crate::error::api_error_to_adbc_error)?; - Ok(result.rows_affected) + // DDL statements (CREATE, DROP, ALTER, TRUNCATE) return a non-meaningful row + // count from Snowflake (typically 1 for "success"). Per the ADBC convention, + // return None (-1 in Python) for DDL so callers can distinguish it from DML. + let rows = if is_ddl(self.query.as_deref().unwrap_or("")) { + None + } else { + result.rows_affected + }; + Ok(rows) } fn execute_schema(&mut self) -> Result { - Err(crate::error::not_implemented("execute_schema")) + let query = self.query.clone().ok_or_else(|| { + Error::with_message_and_status("cannot execute without a query", Status::InvalidState) + })?; + + self.apply_query_tag()?; + + let result = self + .inner + .runtime + .block_on(async { + self.inner + .sf + .statement_set_sql_query(self.stmt_handle, query) + .await?; + self.inner + .sf + .statement_execute_query(self.stmt_handle, None) + .await + }) + .map_err(crate::error::api_error_to_adbc_error)?; + + // Safety: result.stream is a valid FFI stream from sf_core. Ownership is transferred + // to ArrowArrayStreamReader. The C ABI layout is stable per the Arrow C Data Interface. + let raw = + Box::into_raw(result.stream) as *mut arrow_array::ffi_stream::FFI_ArrowArrayStream; + let reader = unsafe { arrow_array::ffi_stream::ArrowArrayStreamReader::from_raw(raw) } + .map_err(|e| Error::with_message_and_status(e.to_string(), Status::IO))?; + // .schema() calls get_schema on the FFI stream without consuming any record batches. + // Dropping the reader invokes the stream's release callback. + Ok(reader.schema().as_ref().clone()) } fn execute_partitions(&mut self) -> Result { @@ -296,6 +424,121 @@ impl adbc_core::Statement for Statement { } } +// ── ConcatReader: chains multiple RecordBatches into a single reader ────────── + +struct ConcatReader { + batches: std::vec::IntoIter, + schema: Arc, +} + +impl Iterator for ConcatReader { + type Item = std::result::Result; + fn next(&mut self) -> Option { + self.batches.next().map(Ok) + } +} + +impl RecordBatchReader for ConcatReader { + fn schema(&self) -> Arc { + self.schema.clone() + } +} + +// ── Parameter substitution ──────────────────────────────────────────────────── + +/// Replaces each `?` placeholder in `query` with the SQL literal value of the +/// corresponding column (1-indexed) in `batch` at `row_idx`. +fn substitute_params(query: &str, batch: &RecordBatch, row_idx: usize) -> Result { + let mut result = String::with_capacity(query.len() * 2); + let mut param_idx = 0usize; + let mut chars = query.chars().peekable(); + + while let Some(ch) = chars.next() { + if ch == '?' { + let col = batch.column(param_idx); + result.push_str(&arrow_value_to_sql_literal(col.as_ref(), row_idx)?); + param_idx += 1; + } else { + result.push(ch); + } + } + Ok(result) +} + +/// Formats an Arrow column value at `row` as a Snowflake SQL literal. +/// NULL → `NULL`; strings are single-quoted; numbers are unquoted. +fn arrow_value_to_sql_literal(arr: &dyn Array, row: usize) -> Result { + if arr.is_null(row) { + return Ok("NULL".to_string()); + } + use arrow_array::{ + BooleanArray, Date32Array, Float32Array, Float64Array, Int16Array, Int32Array, Int64Array, + LargeStringArray, StringArray, + }; + macro_rules! num_lit { + ($T:ty) => { + if let Some(a) = arr.as_any().downcast_ref::<$T>() { + return Ok(format!("{}", a.value(row))); + } + }; + } + num_lit!(Float64Array); + num_lit!(Float32Array); + num_lit!(Int64Array); + num_lit!(Int32Array); + num_lit!(Int16Array); + if let Some(a) = arr.as_any().downcast_ref::() { + return Ok(sql_str_lit(a.value(row))); + } + if let Some(a) = arr.as_any().downcast_ref::() { + return Ok(sql_str_lit(a.value(row))); + } + if let Some(a) = arr.as_any().downcast_ref::() { + return Ok(if a.value(row) { "TRUE" } else { "FALSE" }.to_string()); + } + if let Some(a) = arr.as_any().downcast_ref::() { + return Ok(format!( + "'{}'::DATE", + days_since_epoch_to_date_str(a.value(row) as i64) + )); + } + Err(Error::with_message_and_status( + format!("unsupported bind parameter type: {:?}", arr.data_type()), + Status::NotImplemented, + )) +} + +/// Wraps `s` in single quotes, escaping internal single quotes by doubling them. +fn sql_str_lit(s: &str) -> String { + format!("'{}'", s.replace('\'', "''")) +} + +/// Converts days since Unix epoch (1970-01-01) to a YYYY-MM-DD string. +fn days_since_epoch_to_date_str(days: i64) -> String { + // Algorithm: civil date from days (Gregorian proleptic) + let z = days + 719468; + let era = z.div_euclid(146097); + let doe = z.rem_euclid(146097); + let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; + let y = yoe + era * 400; + let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); + let mp = (5 * doy + 2) / 153; + let d = doy - (153 * mp + 2) / 5 + 1; + let m = if mp < 10 { mp + 3 } else { mp - 9 }; + let y = if m <= 2 { y + 1 } else { y }; + format!("{:04}-{:02}-{:02}", y, m, d) +} + +fn is_ddl(query: &str) -> bool { + let upper = query.trim_start().to_uppercase(); + upper.starts_with("CREATE ") + || upper.starts_with("DROP ") + || upper.starts_with("ALTER ") + || upper.starts_with("TRUNCATE ") + || upper.starts_with("RENAME ") + || upper.starts_with("COMMENT ") +} + #[cfg(test)] mod tests { use super::*; @@ -309,10 +552,13 @@ mod tests { conn_handle: sf_core::apis::database_driver_v1::Handle { id: 0, magic: 0 }, query: None, target_table: None, + ingest_catalog: None, + ingest_schema: None, ingest_mode: None, query_tag: None, use_high_precision: true, timestamp_precision: TimestampPrecision::Nanoseconds, + bound_batches: vec![], } } @@ -332,6 +578,15 @@ mod tests { } } + #[test] + fn execute_schema_without_query_returns_invalid_state() { + let mut stmt = make_stmt(); + match stmt.execute_schema() { + Err(err) => assert_eq!(err.status, adbc_core::error::Status::InvalidState), + Ok(_) => panic!("execute_schema should have returned an error"), + } + } + #[test] fn execute_with_target_table_returns_not_implemented() { let driver = crate::driver::Driver::default(); @@ -341,10 +596,13 @@ mod tests { conn_handle: sf_core::apis::database_driver_v1::Handle { id: 0, magic: 0 }, query: None, target_table: Some("mytable".into()), + ingest_catalog: None, + ingest_schema: None, ingest_mode: None, query_tag: None, use_high_precision: true, timestamp_precision: TimestampPrecision::Nanoseconds, + bound_batches: vec![], }; match stmt.execute() { Err(err) => assert_eq!(err.status, adbc_core::error::Status::NotImplemented), @@ -361,10 +619,13 @@ mod tests { conn_handle: sf_core::apis::database_driver_v1::Handle { id: 0, magic: 0 }, query: None, target_table: Some("mytable".into()), + ingest_catalog: None, + ingest_schema: None, ingest_mode: None, query_tag: None, use_high_precision: true, timestamp_precision: TimestampPrecision::Nanoseconds, + bound_batches: vec![], }; stmt.set_sql_query("SELECT 1").unwrap(); assert!(stmt.target_table.is_none()); diff --git a/rust/tests/integration.rs b/rust/tests/integration.rs index ad6f9ec..49a198d 100644 --- a/rust/tests/integration.rs +++ b/rust/tests/integration.rs @@ -493,9 +493,18 @@ fn test_database_options_round_trip() { "adbc.snowflake.sql.client_option.keep_session_alive", "enabled", ), - ("adbc.snowflake.sql.client_option.disable_telemetry", "enabled"), - ("adbc.snowflake.sql.client_option.cache_mfa_token", "enabled"), - ("adbc.snowflake.sql.client_option.store_temp_creds", "enabled"), + ( + "adbc.snowflake.sql.client_option.disable_telemetry", + "enabled", + ), + ( + "adbc.snowflake.sql.client_option.cache_mfa_token", + "enabled", + ), + ( + "adbc.snowflake.sql.client_option.store_temp_creds", + "enabled", + ), ("adbc.snowflake.sql.client_option.tracing", "debug"), ( "adbc.snowflake.sql.client_option.identity_provider", @@ -565,9 +574,7 @@ fn test_ocsp_fail_open_mode_option() { }; db.set_option( - OptionDatabase::Other( - "adbc.snowflake.sql.client_option.ocsp_fail_open_mode".into(), - ), + OptionDatabase::Other("adbc.snowflake.sql.client_option.ocsp_fail_open_mode".into()), OptionValue::String("enabled".into()), ) .expect("set ocsp_fail_open_mode enabled"); @@ -580,9 +587,7 @@ fn test_ocsp_fail_open_mode_option() { ); db.set_option( - OptionDatabase::Other( - "adbc.snowflake.sql.client_option.ocsp_fail_open_mode".into(), - ), + OptionDatabase::Other("adbc.snowflake.sql.client_option.ocsp_fail_open_mode".into()), OptionValue::String("disabled".into()), ) .expect("set ocsp_fail_open_mode disabled"); @@ -594,3 +599,48 @@ fn test_ocsp_fail_open_mode_option() { "disabled" ); } + +#[test] +fn test_execute_schema() { + use arrow_schema::{DataType, TimeUnit}; + + let Some(mut conn) = make_connection() else { + eprintln!("Skipping: SNOWFLAKE_URI not set"); + return; + }; + + // Schema from a plain SELECT + { + let mut stmt = conn.new_statement().unwrap(); + stmt.set_sql_query("SELECT 1::INTEGER AS n, 'hello'::VARCHAR AS s") + .unwrap(); + let schema = stmt.execute_schema().expect("execute_schema"); + assert_eq!(schema.fields().len(), 2); + } + + // Schema from a table select matches the table definition + { + let mut stmt = conn.new_statement().unwrap(); + stmt.set_sql_query( + "CREATE OR REPLACE TEMP TABLE adbc_rust_schema_test \ + (id NUMBER(10,0), val VARCHAR, ts TIMESTAMP_NTZ)", + ) + .unwrap(); + stmt.execute_update().expect("create table"); + + let mut stmt = conn.new_statement().unwrap(); + stmt.set_sql_query("SELECT * FROM ADBC_RUST_SCHEMA_TEST") + .unwrap(); + let schema = stmt.execute_schema().expect("execute_schema on table"); + assert_eq!(schema.fields().len(), 3); + assert_eq!(schema.field(0).name(), "ID"); + assert_eq!(schema.field(1).name(), "VAL"); + assert_eq!(schema.field(2).name(), "TS"); + assert_eq!(schema.field(0).data_type(), &DataType::Int64); + assert_eq!(schema.field(1).data_type(), &DataType::Utf8); + assert_eq!( + schema.field(2).data_type(), + &DataType::Timestamp(TimeUnit::Microsecond, None) + ); + } +} diff --git a/rust/validation/tests/snowflake.py b/rust/validation/tests/snowflake.py index ee87106..7462e03 100644 --- a/rust/validation/tests/snowflake.py +++ b/rust/validation/tests/snowflake.py @@ -22,21 +22,23 @@ class SnowflakeQuirks(model.DriverQuirks): name = "snowflake" driver = "adbc_driver_snowflake" - driver_name = "ADBC Driver Foundry Driver for Snowflake" + driver_name = "ADBC Snowflake Driver (Rust)" vendor_name = "Snowflake" - vendor_version = re.compile(r"10\.[0-9]+\.[0-9]+") + vendor_version = re.compile(r"\d+\.[0-9]+\.[0-9]+") short_version = "10" features = model.DriverFeatures( connection_get_table_schema=True, + connection_set_current_catalog=True, + connection_set_current_schema=True, connection_transactions=True, get_objects=True, get_objects_constraints_foreign=False, get_objects_constraints_primary=False, get_objects_constraints_unique=False, statement_bind=True, - statement_bulk_ingest=True, - statement_bulk_ingest_catalog=True, - statement_bulk_ingest_schema=True, + statement_bulk_ingest=False, + statement_bulk_ingest_catalog=False, + statement_bulk_ingest_schema=False, statement_bulk_ingest_temporary=False, statement_execute_schema=True, statement_get_parameter_schema=False, @@ -53,7 +55,10 @@ class SnowflakeQuirks(model.DriverQuirks): setup = model.DriverSetup( database={ "uri": model.FromEnv("SNOWFLAKE_URI"), + "adbc.snowflake.sql.db": model.FromEnv("SNOWFLAKE_DATABASE"), + "adbc.snowflake.sql.schema": model.FromEnv("SNOWFLAKE_SCHEMA"), "adbc.snowflake.sql.client_option.use_high_precision": "false", + "adbc.snowflake.sql.client_option.max_timestamp_precision": "microseconds", "timezone": "UTC", }, connection={}, From 704f0f979b5a4f2e0c6f61099da9c15d55a816ec Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Sun, 22 Mar 2026 13:20:37 -0400 Subject: [PATCH 33/76] formatting and xdbc --- rust/src/connection.rs | 48 ++- rust/src/get_objects.rs | 24 +- rust/src/ingest.rs | 458 +++++++++++++++++++++++++++++ rust/src/lib.rs | 1 + rust/src/statement.rs | 164 +++++++++-- rust/validation/tests/snowflake.py | 8 +- 6 files changed, 669 insertions(+), 34 deletions(-) create mode 100644 rust/src/ingest.rs diff --git a/rust/src/connection.rs b/rust/src/connection.rs index 98a71c8..47ed29e 100644 --- a/rust/src/connection.rs +++ b/rust/src/connection.rs @@ -21,6 +21,11 @@ // under the License. // src/connection.rs + +/// Arrow library version reported via get_info(DriverArrowVersion). +/// Must be kept in sync with the `arrow-array` dependency version in Cargo.toml. +const ARROW_VERSION: &str = "v57.3.0"; + use std::collections::HashSet; use std::sync::Arc; @@ -320,7 +325,7 @@ impl adbc_core::Connection for Connection { "ADBC Snowflake Driver (Rust)", env!("CARGO_PKG_VERSION"), vendor_version.as_str(), - "v57.3.0", + ARROW_VERSION, ])) as ArrayRef; let bool_values = Arc::new(BooleanArray::from(vec![true, false])) as ArrayRef; let int64_values = @@ -492,18 +497,41 @@ impl adbc_core::Connection for Connection { for batch in reader { let batch = batch.map_err(|e| Error::with_message_and_status(e.to_string(), Status::IO))?; - if batch.num_columns() < 4 { + use arrow_array::cast::AsArray; + + // Resolve column indices by name (case-insensitive) so a future + // reordering of DESC TABLE columns doesn't silently shift the mapping. + // Known positional defaults from the current Snowflake DESC TABLE schema: + // 0=name, 1=type, 2=kind, 3=null?, 4=default, 5=primary key, + // 6=unique key, 7=check, 8=expression, 9=comment, … + let schema = batch.schema(); + let find = |name: &str, fallback: usize| { + schema + .fields() + .iter() + .position(|f| f.name().eq_ignore_ascii_case(name)) + .unwrap_or(fallback) + }; + let name_col = find("name", 0); + let type_col = find("type", 1); + let null_col = find("null?", 3); + let pk_col = find("primary key", 5); + let comment_col = find("comment", 9); + + if batch.num_columns() <= name_col + || batch.num_columns() <= type_col + || batch.num_columns() <= null_col + { continue; } - use arrow_array::cast::AsArray; - let names = batch.column(0).as_string::(); - let types = batch.column(1).as_string::(); - let nullables = batch.column(3).as_string::(); - // primary_key is column 5; comment is column 9 — present only when - // the result has enough columns (older Snowflake editions may omit them). + let names = batch.column(name_col).as_string::(); + let types = batch.column(type_col).as_string::(); + let nullables = batch.column(null_col).as_string::(); + // primary_key and comment are present only when the result has enough columns. let primary_keys = - (batch.num_columns() > 5).then(|| batch.column(5).as_string::()); - let comments = (batch.num_columns() > 9).then(|| batch.column(9).as_string::()); + (batch.num_columns() > pk_col).then(|| batch.column(pk_col).as_string::()); + let comments = (batch.num_columns() > comment_col) + .then(|| batch.column(comment_col).as_string::()); for i in 0..batch.num_rows() { let type_str = types.value(i); let arrow_type = snowflake_type_to_arrow( diff --git a/rust/src/get_objects.rs b/rust/src/get_objects.rs index cbbef18..f400b42 100644 --- a/rust/src/get_objects.rs +++ b/rust/src/get_objects.rs @@ -57,7 +57,7 @@ fn ilike(p: Option<&str>) -> String { fn info_prefix(catalog_filter: Option<&str>) -> String { match catalog_filter { Some(c) if !c.is_empty() && !c.contains('%') && !c.contains('_') => { - format!("\"{}\".information_schema.", sql_esc(c)) + format!("\"{}\".information_schema.", c.replace('"', "\"\"")) } _ => "information_schema.".to_string(), } @@ -128,12 +128,34 @@ fn cell_to_string(arr: &dyn Array, i: usize) -> Option { if let Some(s) = arr.as_any().downcast_ref::() { return Some(s.value(i).to_string()); } + if let Some(s) = arr.as_any().downcast_ref::() { + return Some(s.value(i).to_string()); + } if let Some(n) = arr.as_any().downcast_ref::() { return Some(n.value(i).to_string()); } if let Some(n) = arr.as_any().downcast_ref::() { return Some(n.value(i).to_string()); } + if let Some(n) = arr.as_any().downcast_ref::() { + return Some(n.value(i).to_string()); + } + // Snowflake NUMBER(p,0) columns (like ordinal_position) arrive as Decimal128 + // when sf_core applies high-precision type mapping. Extract the integer part + // by dividing by 10^scale (scale is typically 0 for integer metadata columns). + if let Some(a) = arr.as_any().downcast_ref::() { + let scale = match a.data_type() { + DataType::Decimal128(_, s) => *s, + _ => 0i8, + }; + let raw = a.value(i); + let value = if scale > 0 { + raw / 10i128.pow(scale as u32) + } else { + raw + }; + return Some(value.to_string()); + } None } diff --git a/rust/src/ingest.rs b/rust/src/ingest.rs new file mode 100644 index 0000000..5e1992b --- /dev/null +++ b/rust/src/ingest.rs @@ -0,0 +1,458 @@ +// Copyright (c) 2026 ADBC Drivers Contributors +// +// This file has been modified from its original version, which is +// under the Apache License: +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +// src/ingest.rs +// +// Bulk ingest implementation using a VALUES-based INSERT pipeline. +// For each bound batch: CREATE TABLE (if needed) then INSERT … VALUES. +// Rows are chunked so individual SQL statements stay well under 1 MB. + +use std::sync::Arc; + +use adbc_core::error::{Error, Result, Status}; +use arrow_array::{Array, RecordBatch}; +use arrow_schema::{DataType, Schema, TimeUnit}; + +use crate::driver::Inner; +use crate::statement::Statement; + +const INSERT_CHUNK_ROWS: usize = 500; + +// ── public entry point ──────────────────────────────────────────────────────── + +pub(crate) fn execute_ingest(stmt: &Statement) -> Result> { + if stmt.bound_batches.is_empty() { + return Err(Error::with_message_and_status( + "ingest requires bound data — call bind() or bind_stream() first", + Status::InvalidState, + )); + } + + let table = stmt.target_table.as_deref().unwrap(); + let qname = qualified_name( + table, + stmt.ingest_catalog.as_deref(), + stmt.ingest_schema.as_deref(), + ); + + let mode = stmt.ingest_mode.as_deref().unwrap_or("adbc.ingest.mode.create"); + let schema = stmt.bound_batches[0].schema(); + + match mode { + "adbc.ingest.mode.create" => { + let ddl = build_create_sql(&qname, &schema, false)?; + run_sql(&stmt.inner, stmt.conn_handle, &ddl)?; + } + "adbc.ingest.mode.append" => { + // table must already exist — no DDL needed + } + "adbc.ingest.mode.replace" => { + run_sql( + &stmt.inner, + stmt.conn_handle, + &format!("DROP TABLE IF EXISTS {qname}"), + )?; + let ddl = build_create_sql(&qname, &schema, false)?; + run_sql(&stmt.inner, stmt.conn_handle, &ddl)?; + } + "adbc.ingest.mode.create_append" => { + let ddl = build_create_sql(&qname, &schema, true)?; + run_sql(&stmt.inner, stmt.conn_handle, &ddl)?; + } + other => { + return Err(Error::with_message_and_status( + format!("unknown ingest mode: {other}"), + Status::InvalidArguments, + )); + } + } + + let mut total = 0i64; + for batch in &stmt.bound_batches { + total += insert_batch(&stmt.inner, stmt.conn_handle, &qname, batch)?; + } + Ok(Some(total)) +} + +// ── DDL helpers ─────────────────────────────────────────────────────────────── + +/// Returns the fully-qualified, double-quoted table name. +fn qualified_name(table: &str, catalog: Option<&str>, schema: Option<&str>) -> String { + let q = |s: &str| format!("\"{}\"", s.replace('"', "\"\"")); + match (catalog, schema) { + (Some(c), Some(s)) => format!("{}.{}.{}", q(c), q(s), q(table)), + (None, Some(s)) => format!("{}.{}", q(s), q(table)), + (Some(c), None) => format!("{}.{}", q(c), q(table)), + (None, None) => q(table), + } +} + +/// Builds `CREATE [OR REPLACE] TABLE [IF NOT EXISTS] (cols…)`. +fn build_create_sql(qname: &str, schema: &Schema, if_not_exists: bool) -> Result { + let mut cols = Vec::with_capacity(schema.fields().len()); + for field in schema.fields() { + let sf_type = to_sf_ddl(field.data_type())?; + let null_clause = if field.is_nullable() { "" } else { " NOT NULL" }; + cols.push(format!( + "\"{}\" {sf_type}{null_clause}", + field.name().replace('"', "\"\"") + )); + } + let exists = if if_not_exists { " IF NOT EXISTS" } else { "" }; + Ok(format!("CREATE TABLE{exists} {qname} ({})", cols.join(", "))) +} + +/// Maps an Arrow DataType to its Snowflake DDL type string. +/// Matches the Go driver's `toSnowflakeType` function. +fn to_sf_ddl(dt: &DataType) -> Result { + Ok(match dt { + DataType::Int8 + | DataType::Int16 + | DataType::Int32 + | DataType::Int64 + | DataType::UInt8 + | DataType::UInt16 + | DataType::UInt32 + | DataType::UInt64 => "integer".to_string(), + + DataType::Float16 | DataType::Float32 | DataType::Float64 => "double".to_string(), + + DataType::Decimal128(p, s) => format!("NUMERIC({p},{s})"), + + DataType::Utf8 | DataType::LargeUtf8 => "text".to_string(), + + DataType::Binary | DataType::LargeBinary => "binary".to_string(), + + DataType::FixedSizeBinary(n) => format!("binary({n})"), + + DataType::Boolean => "boolean".to_string(), + + DataType::Date32 | DataType::Date64 => "date".to_string(), + + DataType::Time32(u) => { + let prec = time_unit_prec(u); + format!("time({prec})") + } + DataType::Time64(u) => { + let prec = time_unit_prec(u); + format!("time({prec})") + } + + DataType::Timestamp(u, tz) => { + let prec = time_unit_prec(u); + if tz.is_some() { + format!("timestamp_ltz({prec})") + } else { + format!("timestamp_ntz({prec})") + } + } + + DataType::List(_) | DataType::LargeList(_) | DataType::FixedSizeList(_, _) => { + "array".to_string() + } + DataType::Struct(_) | DataType::Map(_, _) => "object".to_string(), + + other => { + return Err(Error::with_message_and_status( + format!("unsupported ingest type: {other:?}"), + Status::NotImplemented, + )) + } + }) +} + +fn time_unit_prec(u: &TimeUnit) -> u8 { + match u { + TimeUnit::Second => 0, + TimeUnit::Millisecond => 3, + TimeUnit::Microsecond => 6, + TimeUnit::Nanosecond => 9, + } +} + +// ── INSERT helpers ──────────────────────────────────────────────────────────── + +fn insert_batch(inner: &Arc, conn: sf_core::handle_manager::Handle, qname: &str, batch: &RecordBatch) -> Result { + if batch.num_rows() == 0 { + return Ok(0); + } + + let schema = batch.schema(); + let col_list: Vec = schema + .fields() + .iter() + .map(|f| format!("\"{}\"", f.name().replace('"', "\"\""))) + .collect(); + let col_clause = col_list.join(", "); + + let mut inserted = 0i64; + let mut row = 0usize; + + while row < batch.num_rows() { + let end = (row + INSERT_CHUNK_ROWS).min(batch.num_rows()); + let mut rows_sql = Vec::with_capacity(end - row); + + for r in row..end { + let mut vals = Vec::with_capacity(schema.fields().len()); + for (c, field) in schema.fields().iter().enumerate() { + vals.push(value_to_sql(batch.column(c).as_ref(), r, field.data_type())?); + } + rows_sql.push(format!("({})", vals.join(", "))); + } + + let sql = format!( + "INSERT INTO {qname} ({col_clause}) VALUES {}", + rows_sql.join(", ") + ); + run_sql(inner, conn, &sql)?; + inserted += (end - row) as i64; + row = end; + } + + Ok(inserted) +} + +// ── SQL execution helper ────────────────────────────────────────────────────── + +fn run_sql(inner: &Arc, conn: sf_core::handle_manager::Handle, sql: &str) -> Result<()> { + let tmp = inner + .sf + .statement_new(conn) + .map_err(crate::error::api_error_to_adbc_error)?; + let result = inner.runtime.block_on(async { + inner + .sf + .statement_set_sql_query(tmp, sql.to_string()) + .await?; + inner.sf.statement_execute_query(tmp, None).await + }); + let _ = inner.sf.statement_release(tmp); + result + .map(|_| ()) + .map_err(crate::error::api_error_to_adbc_error) +} + +// ── value formatting ────────────────────────────────────────────────────────── + +/// Formats a single Arrow column value as a Snowflake SQL literal. +fn value_to_sql(arr: &dyn Array, row: usize, dt: &DataType) -> Result { + if arr.is_null(row) { + return Ok("NULL".to_string()); + } + + use arrow_array::{ + BinaryArray, BooleanArray, Date32Array, Decimal128Array, FixedSizeBinaryArray, + Float32Array, Float64Array, Int16Array, Int32Array, Int64Array, Int8Array, + LargeBinaryArray, LargeStringArray, StringArray, Time32MillisecondArray, + Time32SecondArray, Time64MicrosecondArray, Time64NanosecondArray, + TimestampMicrosecondArray, TimestampNanosecondArray, TimestampSecondArray, + TimestampMillisecondArray, UInt16Array, UInt32Array, UInt64Array, UInt8Array, + }; + + macro_rules! num { + ($T:ty) => { + if let Some(a) = arr.as_any().downcast_ref::<$T>() { + return Ok(format!("{}", a.value(row))); + } + }; + } + + num!(Int8Array); + num!(Int16Array); + num!(Int32Array); + num!(Int64Array); + num!(UInt8Array); + num!(UInt16Array); + num!(UInt32Array); + num!(UInt64Array); + // Floats: use {:?} to always emit a decimal or exponent (avoids huge integer strings). + if let Some(a) = arr.as_any().downcast_ref::() { + let v = a.value(row); + return if v.is_finite() { Ok(format!("{:?}", v as f64)) } else { Ok("NULL".to_string()) }; + } + if let Some(a) = arr.as_any().downcast_ref::() { + let v = a.value(row); + return if v.is_finite() { Ok(format!("{v:?}")) } else { Ok("NULL".to_string()) }; + } + + if let Some(a) = arr.as_any().downcast_ref::() { + return Ok(if a.value(row) { "TRUE" } else { "FALSE" }.to_string()); + } + + if let Some(a) = arr.as_any().downcast_ref::() { + return Ok(sql_str(a.value(row))); + } + if let Some(a) = arr.as_any().downcast_ref::() { + return Ok(sql_str(a.value(row))); + } + + if let Some(a) = arr.as_any().downcast_ref::() { + return Ok(sql_binary(a.value(row))); + } + if let Some(a) = arr.as_any().downcast_ref::() { + return Ok(sql_binary(a.value(row))); + } + if let Some(a) = arr.as_any().downcast_ref::() { + return Ok(sql_binary(a.value(row))); + } + + if let Some(a) = arr.as_any().downcast_ref::() { + let days = a.value(row) as i64; + return Ok(format!("'{}'::DATE", days_to_date(days))); + } + + // TIME + if let Some(a) = arr.as_any().downcast_ref::() { + return Ok(format!("'{}'::TIME(9)", ns_to_time(a.value(row)))); + } + if let Some(a) = arr.as_any().downcast_ref::() { + return Ok(format!("'{}'::TIME(6)", us_to_time(a.value(row)))); + } + if let Some(a) = arr.as_any().downcast_ref::() { + return Ok(format!("'{}'::TIME(3)", ms_to_time(a.value(row) as i64))); + } + if let Some(a) = arr.as_any().downcast_ref::() { + return Ok(format!("'{}'::TIME(0)", s_to_time(a.value(row) as i64))); + } + + // TIMESTAMP — use TO_TIMESTAMP_NTZ / TO_TIMESTAMP_LTZ with epoch + precision + if let Some(a) = arr.as_any().downcast_ref::() { + let v = a.value(row); + return Ok(match dt { + DataType::Timestamp(_, Some(_)) => format!("TO_TIMESTAMP_LTZ({v}, 9)"), + _ => format!("TO_TIMESTAMP_NTZ({v}, 9)"), + }); + } + if let Some(a) = arr.as_any().downcast_ref::() { + let v = a.value(row); + return Ok(match dt { + DataType::Timestamp(_, Some(_)) => format!("TO_TIMESTAMP_LTZ({v}, 6)"), + _ => format!("TO_TIMESTAMP_NTZ({v}, 6)"), + }); + } + if let Some(a) = arr.as_any().downcast_ref::() { + let v = a.value(row); + return Ok(match dt { + DataType::Timestamp(_, Some(_)) => format!("TO_TIMESTAMP_LTZ({v}, 3)"), + _ => format!("TO_TIMESTAMP_NTZ({v}, 3)"), + }); + } + if let Some(a) = arr.as_any().downcast_ref::() { + let v = a.value(row); + return Ok(match dt { + DataType::Timestamp(_, Some(_)) => format!("TO_TIMESTAMP_LTZ({v}, 0)"), + _ => format!("TO_TIMESTAMP_NTZ({v}, 0)"), + }); + } + + // DECIMAL + if let Some(a) = arr.as_any().downcast_ref::() { + if let DataType::Decimal128(_, scale) = dt { + return Ok(decimal128_to_str(a.value(row), *scale)); + } + } + + Err(Error::with_message_and_status( + format!("unsupported ingest value type: {dt:?}"), + Status::NotImplemented, + )) +} + +// ── value formatting helpers ────────────────────────────────────────────────── + +fn sql_str(s: &str) -> String { + // Double backslashes before doubling single quotes — see statement.rs sql_str_lit. + format!("'{}'", s.replace('\\', "\\\\").replace('\'', "''")) +} + +fn sql_binary(b: &[u8]) -> String { + let hex: String = b.iter().map(|byte| format!("{byte:02x}")).collect(); + format!("TO_BINARY('{hex}', 'HEX')") +} + +fn days_to_date(days: i64) -> String { + // Civil date algorithm (same as statement.rs) + let z = days + 719468; + let era = z.div_euclid(146097); + let doe = z.rem_euclid(146097); + let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; + let y = yoe + era * 400; + let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); + let mp = (5 * doy + 2) / 153; + let d = doy - (153 * mp + 2) / 5 + 1; + let m = if mp < 10 { mp + 3 } else { mp - 9 }; + let y = if m <= 2 { y + 1 } else { y }; + format!("{y:04}-{m:02}-{d:02}") +} + +fn ns_to_time(ns: i64) -> String { + let h = ns / 3_600_000_000_000; + let r = ns % 3_600_000_000_000; + let m = r / 60_000_000_000; + let r = r % 60_000_000_000; + let s = r / 1_000_000_000; + let f = r % 1_000_000_000; + format!("{h:02}:{m:02}:{s:02}.{f:09}") +} + +fn us_to_time(us: i64) -> String { + let h = us / 3_600_000_000; + let r = us % 3_600_000_000; + let m = r / 60_000_000; + let r = r % 60_000_000; + let s = r / 1_000_000; + let f = r % 1_000_000; + format!("{h:02}:{m:02}:{s:02}.{f:06}") +} + +fn ms_to_time(ms: i64) -> String { + let h = ms / 3_600_000; + let r = ms % 3_600_000; + let m = r / 60_000; + let r = r % 60_000; + let s = r / 1_000; + let f = r % 1_000; + format!("{h:02}:{m:02}:{s:02}.{f:03}") +} + +fn s_to_time(s: i64) -> String { + let h = s / 3600; + let r = s % 3600; + let m = r / 60; + let s = r % 60; + format!("{h:02}:{m:02}:{s:02}") +} + +fn decimal128_to_str(raw: i128, scale: i8) -> String { + if scale <= 0 { + // Negative scale means multiply by 10^(-scale); just format as integer + // (Arrow guarantees the stored value is already the scaled integer). + return format!("{raw}"); + } + let scale = scale as usize; + let neg = raw < 0; + let abs = raw.unsigned_abs(); + let s = format!("{abs:0>width$}", width = scale + 1); + let int_part = &s[..s.len() - scale]; + let frac_part = &s[s.len() - scale..]; + format!("{}{int_part}.{frac_part}", if neg { "-" } else { "" }) +} diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 4dd972d..287dbd4 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -33,6 +33,7 @@ mod connection; pub use connection::Connection; mod get_objects; +mod ingest; mod statement; pub use statement::Statement; diff --git a/rust/src/statement.rs b/rust/src/statement.rs index 4d518d2..1d0ec58 100644 --- a/rust/src/statement.rs +++ b/rust/src/statement.rs @@ -279,9 +279,12 @@ impl adbc_core::Statement for Statement { #[allow(refining_impl_trait)] fn execute(&mut self) -> Result> { if self.target_table.is_some() { - return Err(crate::error::not_implemented( - "bulk ingestion (target_table) is not yet implemented", + // Ingest via execute() — run the ingest and return an empty reader. + crate::ingest::execute_ingest(self)?; + let batch = arrow_array::RecordBatch::new_empty(Arc::new( + arrow_schema::Schema::empty(), )); + return Ok(Box::new(crate::connection::SingleBatchReader::new(batch))); } let query = self.query.clone().ok_or_else(|| { Error::with_message_and_status("cannot execute without a query", Status::InvalidState) @@ -320,9 +323,7 @@ impl adbc_core::Statement for Statement { fn execute_update(&mut self) -> Result> { if self.target_table.is_some() { - return Err(crate::error::not_implemented( - "bulk ingestion (target_table) is not yet implemented", - )); + return crate::ingest::execute_ingest(self); } let query = self.query.clone().ok_or_else(|| { Error::with_message_and_status("cannot execute without a query", Status::InvalidState) @@ -330,6 +331,34 @@ impl adbc_core::Statement for Statement { self.apply_query_tag()?; + // Parameterised DML: execute once per bound row and sum row counts. + if !self.bound_batches.is_empty() { + let mut total: i64 = 0; + for bound_batch in &self.bound_batches { + for row_idx in 0..bound_batch.num_rows() { + let sql = substitute_params(&query, bound_batch, row_idx)?; + let result = self + .inner + .runtime + .block_on(async { + self.inner + .sf + .statement_set_sql_query(self.stmt_handle, sql) + .await?; + self.inner + .sf + .statement_execute_query(self.stmt_handle, None) + .await + }) + .map_err(crate::error::api_error_to_adbc_error)?; + // Release the stream; we only care about rows_affected. + drop(result.stream); + total += result.rows_affected.unwrap_or(0); + } + } + return Ok(if is_ddl(&query) { None } else { Some(total) }); + } + let result = self .inner .runtime @@ -447,19 +476,76 @@ impl RecordBatchReader for ConcatReader { // ── Parameter substitution ──────────────────────────────────────────────────── /// Replaces each `?` placeholder in `query` with the SQL literal value of the -/// corresponding column (1-indexed) in `batch` at `row_idx`. +/// corresponding bound column at `row_idx`. +/// +/// Skips `?` inside SQL string literals (`'…'`), line comments (`--…`), and +/// block comments (`/*…*/`) so only true parameter markers are substituted. +/// Returns `InvalidArguments` if there are more `?` markers than bound columns. fn substitute_params(query: &str, batch: &RecordBatch, row_idx: usize) -> Result { let mut result = String::with_capacity(query.len() * 2); let mut param_idx = 0usize; let mut chars = query.chars().peekable(); while let Some(ch) = chars.next() { - if ch == '?' { - let col = batch.column(param_idx); - result.push_str(&arrow_value_to_sql_literal(col.as_ref(), row_idx)?); - param_idx += 1; - } else { - result.push(ch); + match ch { + // SQL string literal — copy verbatim; '' is an escaped quote (stay in string) + '\'' => { + result.push('\''); + loop { + match chars.next() { + None => break, + Some('\'') => { + result.push('\''); + if chars.peek() == Some(&'\'') { + result.push(chars.next().unwrap()); // escaped '' + } else { + break; // end of string + } + } + Some(c) => result.push(c), + } + } + } + // Line comment -- copy until end of line + '-' if chars.peek() == Some(&'-') => { + result.push('-'); + result.push(chars.next().unwrap()); + for c in chars.by_ref() { + result.push(c); + if c == '\n' { + break; + } + } + } + // Block comment /* … */ — copy verbatim + '/' if chars.peek() == Some(&'*') => { + result.push('/'); + result.push(chars.next().unwrap()); + let mut prev = '\0'; + for c in chars.by_ref() { + result.push(c); + if prev == '*' && c == '/' { + break; + } + prev = c; + } + } + // Parameter placeholder + '?' => { + if param_idx >= batch.num_columns() { + return Err(Error::with_message_and_status( + format!( + "query has more '?' placeholders than bound columns (have {})", + batch.num_columns() + ), + Status::InvalidArguments, + )); + } + let col = batch.column(param_idx); + result.push_str(&arrow_value_to_sql_literal(col.as_ref(), row_idx)?); + param_idx += 1; + } + c => result.push(c), } } Ok(result) @@ -482,8 +568,25 @@ fn arrow_value_to_sql_literal(arr: &dyn Array, row: usize) -> Result { } }; } - num_lit!(Float64Array); - num_lit!(Float32Array); + // Floats need {:?} format which always emits a decimal point or exponent + // (e.g. "3.14", "1.7976931348623157e308"). The {} Display format may produce + // a huge integer string (e.g. "179769300...") that Snowflake rejects. + if let Some(a) = arr.as_any().downcast_ref::() { + let v = a.value(row); + return if v.is_finite() { + Ok(format!("{v:?}")) + } else { + Ok("NULL".to_string()) + }; + } + if let Some(a) = arr.as_any().downcast_ref::() { + let v = a.value(row); + return if v.is_finite() { + Ok(format!("{:?}", v as f64)) + } else { + Ok("NULL".to_string()) + }; + } num_lit!(Int64Array); num_lit!(Int32Array); num_lit!(Int16Array); @@ -508,9 +611,12 @@ fn arrow_value_to_sql_literal(arr: &dyn Array, row: usize) -> Result { )) } -/// Wraps `s` in single quotes, escaping internal single quotes by doubling them. +/// Wraps `s` in single quotes. +/// Backslashes are doubled first (some Snowflake sessions treat `\'` as an escape +/// sequence, which would prematurely close the literal), then single quotes are +/// doubled per ANSI SQL. fn sql_str_lit(s: &str) -> String { - format!("'{}'", s.replace('\'', "''")) + format!("'{}'", s.replace('\\', "\\\\").replace('\'', "''")) } /// Converts days since Unix epoch (1970-01-01) to a YYYY-MM-DD string. @@ -529,8 +635,28 @@ fn days_since_epoch_to_date_str(days: i64) -> String { format!("{:04}-{:02}-{:02}", y, m, d) } +/// Strips leading SQL whitespace, line comments (`--…`), and block +/// comments (`/*…*/`) from `query`, returning the remaining slice. +fn strip_sql_comments(query: &str) -> &str { + let mut s = query.trim_start(); + loop { + if s.starts_with("--") { + s = s[s.find('\n').map(|i| i + 1).unwrap_or(s.len())..].trim_start(); + } else if s.starts_with("/*") { + if let Some(end) = s.find("*/") { + s = s[end + 2..].trim_start(); + } else { + break; + } + } else { + break; + } + } + s +} + fn is_ddl(query: &str) -> bool { - let upper = query.trim_start().to_uppercase(); + let upper = strip_sql_comments(query).to_uppercase(); upper.starts_with("CREATE ") || upper.starts_with("DROP ") || upper.starts_with("ALTER ") @@ -588,7 +714,7 @@ mod tests { } #[test] - fn execute_with_target_table_returns_not_implemented() { + fn execute_with_target_table_no_data_returns_invalid_state() { let driver = crate::driver::Driver::default(); let mut stmt = Statement { inner: driver.inner.clone(), @@ -605,7 +731,7 @@ mod tests { bound_batches: vec![], }; match stmt.execute() { - Err(err) => assert_eq!(err.status, adbc_core::error::Status::NotImplemented), + Err(err) => assert_eq!(err.status, adbc_core::error::Status::InvalidState), Ok(_) => panic!("execute should have returned an error"), } } diff --git a/rust/validation/tests/snowflake.py b/rust/validation/tests/snowflake.py index 7462e03..7345402 100644 --- a/rust/validation/tests/snowflake.py +++ b/rust/validation/tests/snowflake.py @@ -36,10 +36,10 @@ class SnowflakeQuirks(model.DriverQuirks): get_objects_constraints_primary=False, get_objects_constraints_unique=False, statement_bind=True, - statement_bulk_ingest=False, - statement_bulk_ingest_catalog=False, - statement_bulk_ingest_schema=False, - statement_bulk_ingest_temporary=False, + statement_bulk_ingest=True, + statement_bulk_ingest_catalog=True, + statement_bulk_ingest_schema=True, + statement_bulk_ingest_temporary=True, statement_execute_schema=True, statement_get_parameter_schema=False, statement_prepare=True, From ca73ae5e4880d866aab9be415e30ff2f5bc172a5 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Sun, 22 Mar 2026 13:23:11 -0400 Subject: [PATCH 34/76] clippy and fmt --- rust/src/connection.rs | 14 ++++++------ rust/src/get_objects.rs | 6 ------ rust/src/ingest.rs | 47 ++++++++++++++++++++++++++++++----------- rust/src/statement.rs | 23 +++++++------------- 4 files changed, 50 insertions(+), 40 deletions(-) diff --git a/rust/src/connection.rs b/rust/src/connection.rs index 47ed29e..aabee1d 100644 --- a/rust/src/connection.rs +++ b/rust/src/connection.rs @@ -512,11 +512,11 @@ impl adbc_core::Connection for Connection { .position(|f| f.name().eq_ignore_ascii_case(name)) .unwrap_or(fallback) }; - let name_col = find("name", 0); - let type_col = find("type", 1); - let null_col = find("null?", 3); - let pk_col = find("primary key", 5); - let comment_col = find("comment", 9); + let name_col = find("name", 0); + let type_col = find("type", 1); + let null_col = find("null?", 3); + let pk_col = find("primary key", 5); + let comment_col = find("comment", 9); if batch.num_columns() <= name_col || batch.num_columns() <= type_col @@ -524,8 +524,8 @@ impl adbc_core::Connection for Connection { { continue; } - let names = batch.column(name_col).as_string::(); - let types = batch.column(type_col).as_string::(); + let names = batch.column(name_col).as_string::(); + let types = batch.column(type_col).as_string::(); let nullables = batch.column(null_col).as_string::(); // primary_key and comment are present only when the result has enough columns. let primary_keys = diff --git a/rust/src/get_objects.rs b/rust/src/get_objects.rs index f400b42..9fed007 100644 --- a/rust/src/get_objects.rs +++ b/rust/src/get_objects.rs @@ -437,12 +437,6 @@ fn build_batch(entries: &[CatalogEntry], depth: &ObjectDepth) -> Result = entries diff --git a/rust/src/ingest.rs b/rust/src/ingest.rs index 5e1992b..9220c14 100644 --- a/rust/src/ingest.rs +++ b/rust/src/ingest.rs @@ -54,7 +54,10 @@ pub(crate) fn execute_ingest(stmt: &Statement) -> Result> { stmt.ingest_schema.as_deref(), ); - let mode = stmt.ingest_mode.as_deref().unwrap_or("adbc.ingest.mode.create"); + let mode = stmt + .ingest_mode + .as_deref() + .unwrap_or("adbc.ingest.mode.create"); let schema = stmt.bound_batches[0].schema(); match mode { @@ -118,7 +121,10 @@ fn build_create_sql(qname: &str, schema: &Schema, if_not_exists: bool) -> Result )); } let exists = if if_not_exists { " IF NOT EXISTS" } else { "" }; - Ok(format!("CREATE TABLE{exists} {qname} ({})", cols.join(", "))) + Ok(format!( + "CREATE TABLE{exists} {qname} ({})", + cols.join(", ") + )) } /// Maps an Arrow DataType to its Snowflake DDL type string. @@ -175,7 +181,7 @@ fn to_sf_ddl(dt: &DataType) -> Result { return Err(Error::with_message_and_status( format!("unsupported ingest type: {other:?}"), Status::NotImplemented, - )) + )); } }) } @@ -191,7 +197,12 @@ fn time_unit_prec(u: &TimeUnit) -> u8 { // ── INSERT helpers ──────────────────────────────────────────────────────────── -fn insert_batch(inner: &Arc, conn: sf_core::handle_manager::Handle, qname: &str, batch: &RecordBatch) -> Result { +fn insert_batch( + inner: &Arc, + conn: sf_core::handle_manager::Handle, + qname: &str, + batch: &RecordBatch, +) -> Result { if batch.num_rows() == 0 { return Ok(0); } @@ -214,7 +225,11 @@ fn insert_batch(inner: &Arc, conn: sf_core::handle_manager::Handle, qname for r in row..end { let mut vals = Vec::with_capacity(schema.fields().len()); for (c, field) in schema.fields().iter().enumerate() { - vals.push(value_to_sql(batch.column(c).as_ref(), r, field.data_type())?); + vals.push(value_to_sql( + batch.column(c).as_ref(), + r, + field.data_type(), + )?); } rows_sql.push(format!("({})", vals.join(", "))); } @@ -261,11 +276,11 @@ fn value_to_sql(arr: &dyn Array, row: usize, dt: &DataType) -> Result { use arrow_array::{ BinaryArray, BooleanArray, Date32Array, Decimal128Array, FixedSizeBinaryArray, - Float32Array, Float64Array, Int16Array, Int32Array, Int64Array, Int8Array, - LargeBinaryArray, LargeStringArray, StringArray, Time32MillisecondArray, - Time32SecondArray, Time64MicrosecondArray, Time64NanosecondArray, - TimestampMicrosecondArray, TimestampNanosecondArray, TimestampSecondArray, - TimestampMillisecondArray, UInt16Array, UInt32Array, UInt64Array, UInt8Array, + Float32Array, Float64Array, Int8Array, Int16Array, Int32Array, Int64Array, + LargeBinaryArray, LargeStringArray, StringArray, Time32MillisecondArray, Time32SecondArray, + Time64MicrosecondArray, Time64NanosecondArray, TimestampMicrosecondArray, + TimestampMillisecondArray, TimestampNanosecondArray, TimestampSecondArray, UInt8Array, + UInt16Array, UInt32Array, UInt64Array, }; macro_rules! num { @@ -287,11 +302,19 @@ fn value_to_sql(arr: &dyn Array, row: usize, dt: &DataType) -> Result { // Floats: use {:?} to always emit a decimal or exponent (avoids huge integer strings). if let Some(a) = arr.as_any().downcast_ref::() { let v = a.value(row); - return if v.is_finite() { Ok(format!("{:?}", v as f64)) } else { Ok("NULL".to_string()) }; + return if v.is_finite() { + Ok(format!("{:?}", v as f64)) + } else { + Ok("NULL".to_string()) + }; } if let Some(a) = arr.as_any().downcast_ref::() { let v = a.value(row); - return if v.is_finite() { Ok(format!("{v:?}")) } else { Ok("NULL".to_string()) }; + return if v.is_finite() { + Ok(format!("{v:?}")) + } else { + Ok("NULL".to_string()) + }; } if let Some(a) = arr.as_any().downcast_ref::() { diff --git a/rust/src/statement.rs b/rust/src/statement.rs index 1d0ec58..45a810c 100644 --- a/rust/src/statement.rs +++ b/rust/src/statement.rs @@ -29,7 +29,7 @@ use adbc_core::{ options::{OptionStatement, OptionValue}, }; use arrow_array::{Array, RecordBatch, RecordBatchReader}; -use arrow_schema::{DataType, Schema}; +use arrow_schema::{Schema}; use sf_core::apis::database_driver_v1::Handle; use crate::driver::{Inner, TimestampPrecision}; @@ -179,10 +179,7 @@ impl Statement { /// Parameter values are substituted directly as SQL literals — this avoids /// relying on sf_core's JSON binding path and works with all Snowflake /// server versions without session configuration. - fn execute_bound( - &self, - query: String, - ) -> Result> { + fn execute_bound(&self, query: String) -> Result> { let mut all_batches: Vec = Vec::new(); let mut result_schema: Option> = None; @@ -210,17 +207,14 @@ impl Statement { as *mut arrow_array::ffi_stream::FFI_ArrowArrayStream; let reader = unsafe { arrow_array::ffi_stream::ArrowArrayStreamReader::from_raw(raw) } - .map_err(|e| { - Error::with_message_and_status(e.to_string(), Status::IO) - })?; + .map_err(|e| Error::with_message_and_status(e.to_string(), Status::IO))?; if result_schema.is_none() { result_schema = Some(reader.schema()); } for batch in reader { - let batch = batch.map_err(|e| { - Error::with_message_and_status(e.to_string(), Status::IO) - })?; + let batch = batch + .map_err(|e| Error::with_message_and_status(e.to_string(), Status::IO))?; all_batches.push(batch); } } @@ -281,9 +275,8 @@ impl adbc_core::Statement for Statement { if self.target_table.is_some() { // Ingest via execute() — run the ingest and return an empty reader. crate::ingest::execute_ingest(self)?; - let batch = arrow_array::RecordBatch::new_empty(Arc::new( - arrow_schema::Schema::empty(), - )); + let batch = + arrow_array::RecordBatch::new_empty(Arc::new(arrow_schema::Schema::empty())); return Ok(Box::new(crate::connection::SingleBatchReader::new(batch))); } let query = self.query.clone().ok_or_else(|| { @@ -558,7 +551,7 @@ fn arrow_value_to_sql_literal(arr: &dyn Array, row: usize) -> Result { return Ok("NULL".to_string()); } use arrow_array::{ - BooleanArray, Date32Array, Float32Array, Float64Array, Int16Array, Int32Array, Int64Array, + BooleanArray, Date32Array, Int16Array, Int32Array, Int64Array, LargeStringArray, StringArray, }; macro_rules! num_lit { From 822629ca5c3b8b2089e7b0c08e91637f99fc9c01 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Mon, 23 Mar 2026 13:52:44 -0400 Subject: [PATCH 35/76] get_objects impl --- rust/src/get_objects.rs | 32 ++++++++++++++++++++---- rust/src/ingest.rs | 40 ++++++++++++++++++------------ rust/src/statement.rs | 13 +++++++--- rust/validation/tests/snowflake.py | 5 ++-- 4 files changed, 63 insertions(+), 27 deletions(-) diff --git a/rust/src/get_objects.rs b/rust/src/get_objects.rs index 9fed007..353380a 100644 --- a/rust/src/get_objects.rs +++ b/rust/src/get_objects.rs @@ -140,6 +140,14 @@ fn cell_to_string(arr: &dyn Array, i: usize) -> Option { if let Some(n) = arr.as_any().downcast_ref::() { return Some(n.value(i).to_string()); } + // Some NUMBER(p,0) columns (like ordinal_position) arrive as Float64 in the + // sf_core Arrow stream for metadata queries; truncate to integer string. + if let Some(n) = arr.as_any().downcast_ref::() { + return Some((n.value(i) as i64).to_string()); + } + if let Some(n) = arr.as_any().downcast_ref::() { + return Some((n.value(i) as i64).to_string()); + } // Snowflake NUMBER(p,0) columns (like ordinal_position) arrive as Decimal128 // when sf_core applies high-precision type mapping. Extract the integer part // by dividing by 10^scale (scale is typically 0 for integer metadata columns). @@ -149,9 +157,11 @@ fn cell_to_string(arr: &dyn Array, i: usize) -> Option { _ => 0i8, }; let raw = a.value(i); - let value = if scale > 0 { + let value = if scale > 0 && scale <= 38 { raw / 10i128.pow(scale as u32) } else { + // scale == 0: no adjustment; scale > 38: i128 exponent would overflow, + // return the raw representation rather than panicking. raw }; return Some(value.to_string()); @@ -355,6 +365,13 @@ fn collect( ), )?; + // Track 1-based column position per (catalog, schema, table). + // The query is already sorted by ordinal_position so the rows arrive in + // column order; we count them ourselves rather than trusting the database + // value, which varies across Snowflake environments. + let mut col_counter: std::collections::HashMap<(String, String, String), i32> = + Default::default(); + for row in &col_rows { if row.len() < 13 { continue; @@ -375,10 +392,15 @@ fn collect( 0 } }); - let ordinal = row[4] - .as_deref() - .and_then(|s| s.parse::().ok()) - .unwrap_or(0); + // Compute 1-based ordinal from insertion order (SQL is ordered by + // ordinal_position so this matches the database's sort order). + let ordinal = { + let c = col_counter + .entry((cat.clone(), sch.clone(), tbl.clone())) + .or_insert(0); + *c += 1; + *c + }; table.columns.push(ColEntry { name: col_name.clone(), ordinal_position: ordinal, diff --git a/rust/src/ingest.rs b/rust/src/ingest.rs index 9220c14..6f32020 100644 --- a/rust/src/ingest.rs +++ b/rust/src/ingest.rs @@ -47,7 +47,9 @@ pub(crate) fn execute_ingest(stmt: &Statement) -> Result> { )); } - let table = stmt.target_table.as_deref().unwrap(); + let table = stmt.target_table.as_deref().ok_or_else(|| { + Error::with_message_and_status("target_table not set", Status::InvalidState) + })?; let qname = qualified_name( table, stmt.ingest_catalog.as_deref(), @@ -172,10 +174,19 @@ fn to_sf_ddl(dt: &DataType) -> Result { } } - DataType::List(_) | DataType::LargeList(_) | DataType::FixedSizeList(_, _) => { - "array".to_string() + // List/Struct/Map are intentionally excluded: DDL generation would succeed + // but value_to_sql has no handler for these types, causing every data row + // to fail. Return NotImplemented so callers get a clear error up-front. + DataType::List(_) + | DataType::LargeList(_) + | DataType::FixedSizeList(_, _) + | DataType::Struct(_) + | DataType::Map(_, _) => { + return Err(Error::with_message_and_status( + format!("ingest of nested type {dt:?} is not yet supported"), + Status::NotImplemented, + )) } - DataType::Struct(_) | DataType::Map(_, _) => "object".to_string(), other => { return Err(Error::with_message_and_status( @@ -300,6 +311,8 @@ fn value_to_sql(arr: &dyn Array, row: usize, dt: &DataType) -> Result { num!(UInt32Array); num!(UInt64Array); // Floats: use {:?} to always emit a decimal or exponent (avoids huge integer strings). + // NaN and ±Inf cannot be represented in SQL; they are mapped to NULL, which is a + // deliberate lossy conversion — callers should avoid ingesting non-finite values. if let Some(a) = arr.as_any().downcast_ref::() { let v = a.value(row); return if v.is_finite() { @@ -342,6 +355,11 @@ fn value_to_sql(arr: &dyn Array, row: usize, dt: &DataType) -> Result { let days = a.value(row) as i64; return Ok(format!("'{}'::DATE", days_to_date(days))); } + if let Some(a) = arr.as_any().downcast_ref::() { + // Date64 stores milliseconds since epoch; divide to get days. + let days = a.value(row) / 86_400_000; + return Ok(format!("'{}'::DATE", days_to_date(days))); + } // TIME if let Some(a) = arr.as_any().downcast_ref::() { @@ -412,19 +430,9 @@ fn sql_binary(b: &[u8]) -> String { format!("TO_BINARY('{hex}', 'HEX')") } +/// Thin wrapper so ingest.rs shares the single implementation in statement.rs. fn days_to_date(days: i64) -> String { - // Civil date algorithm (same as statement.rs) - let z = days + 719468; - let era = z.div_euclid(146097); - let doe = z.rem_euclid(146097); - let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; - let y = yoe + era * 400; - let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); - let mp = (5 * doy + 2) / 153; - let d = doy - (153 * mp + 2) / 5 + 1; - let m = if mp < 10 { mp + 3 } else { mp - 9 }; - let y = if m <= 2 { y + 1 } else { y }; - format!("{y:04}-{m:02}-{d:02}") + crate::statement::days_since_epoch_to_date_str(days) } fn ns_to_time(ns: i64) -> String { diff --git a/rust/src/statement.rs b/rust/src/statement.rs index 45a810c..03fb714 100644 --- a/rust/src/statement.rs +++ b/rust/src/statement.rs @@ -344,8 +344,15 @@ impl adbc_core::Statement for Statement { .await }) .map_err(crate::error::api_error_to_adbc_error)?; - // Release the stream; we only care about rows_affected. - drop(result.stream); + // Drain the stream via ArrowArrayStreamReader so sf_core's + // release callback fires before the handle is reused. + let raw = Box::into_raw(result.stream) + as *mut arrow_array::ffi_stream::FFI_ArrowArrayStream; + if let Ok(reader) = + unsafe { arrow_array::ffi_stream::ArrowArrayStreamReader::from_raw(raw) } + { + for _ in reader {} // consume all batches to trigger release + } total += result.rows_affected.unwrap_or(0); } } @@ -613,7 +620,7 @@ fn sql_str_lit(s: &str) -> String { } /// Converts days since Unix epoch (1970-01-01) to a YYYY-MM-DD string. -fn days_since_epoch_to_date_str(days: i64) -> String { +pub(crate) fn days_since_epoch_to_date_str(days: i64) -> String { // Algorithm: civil date from days (Gregorian proleptic) let z = days + 719468; let era = z.div_euclid(146097); diff --git a/rust/validation/tests/snowflake.py b/rust/validation/tests/snowflake.py index 7345402..2dcfa81 100644 --- a/rust/validation/tests/snowflake.py +++ b/rust/validation/tests/snowflake.py @@ -39,7 +39,7 @@ class SnowflakeQuirks(model.DriverQuirks): statement_bulk_ingest=True, statement_bulk_ingest_catalog=True, statement_bulk_ingest_schema=True, - statement_bulk_ingest_temporary=True, + statement_bulk_ingest_temporary=False, statement_execute_schema=True, statement_get_parameter_schema=False, statement_prepare=True, @@ -57,8 +57,7 @@ class SnowflakeQuirks(model.DriverQuirks): "uri": model.FromEnv("SNOWFLAKE_URI"), "adbc.snowflake.sql.db": model.FromEnv("SNOWFLAKE_DATABASE"), "adbc.snowflake.sql.schema": model.FromEnv("SNOWFLAKE_SCHEMA"), - "adbc.snowflake.sql.client_option.use_high_precision": "false", - "adbc.snowflake.sql.client_option.max_timestamp_precision": "microseconds", + "adbc.snowflake.sql.client_option.use_high_precision": "false", "timezone": "UTC", }, connection={}, From 707ee048742c99f87ad95cf565f6b8833d85e05c Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Thu, 26 Mar 2026 22:22:16 -0400 Subject: [PATCH 36/76] feat(deps): add arrow-cast dependency for integer type upcasting --- rust/Cargo.lock | 158 ++++++++++++++++++++++++++++++------------------ rust/Cargo.toml | 3 +- 2 files changed, 102 insertions(+), 59 deletions(-) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 9d88442..b70247e 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -10,6 +10,7 @@ dependencies = [ "adbc_ffi", "arrow-array 57.3.0", "arrow-buffer 57.3.0", + "arrow-cast 57.3.0", "arrow-schema 57.3.0", "sf_core", "tokio", @@ -80,21 +81,6 @@ dependencies = [ "libc", ] -[[package]] -name = "anstream" -version = "0.6.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" -dependencies = [ - "anstyle", - "anstyle-parse 0.2.7", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - [[package]] name = "anstream" version = "1.0.0" @@ -102,7 +88,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", - "anstyle-parse 1.0.0", + "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", @@ -116,15 +102,6 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" -[[package]] -name = "anstyle-parse" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" -dependencies = [ - "utf8parse", -] - [[package]] name = "anstyle-parse" version = "1.0.0" @@ -169,15 +146,15 @@ dependencies = [ "arrow-arith", "arrow-array 56.2.0", "arrow-buffer 56.2.0", - "arrow-cast", + "arrow-cast 56.2.0", "arrow-csv", "arrow-data 56.2.0", "arrow-ipc", "arrow-json", - "arrow-ord", + "arrow-ord 56.2.0", "arrow-row", "arrow-schema 56.2.0", - "arrow-select", + "arrow-select 56.2.0", "arrow-string", ] @@ -262,7 +239,7 @@ dependencies = [ "arrow-buffer 56.2.0", "arrow-data 56.2.0", "arrow-schema 56.2.0", - "arrow-select", + "arrow-select 56.2.0", "atoi", "base64 0.22.1", "chrono", @@ -272,6 +249,27 @@ dependencies = [ "ryu", ] +[[package]] +name = "arrow-cast" +version = "57.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "646bbb821e86fd57189c10b4fcdaa941deaf4181924917b0daa92735baa6ada5" +dependencies = [ + "arrow-array 57.3.0", + "arrow-buffer 57.3.0", + "arrow-data 57.3.0", + "arrow-ord 57.3.0", + "arrow-schema 57.3.0", + "arrow-select 57.3.0", + "atoi", + "base64 0.22.1", + "chrono", + "half", + "lexical-core", + "num-traits", + "ryu", +] + [[package]] name = "arrow-csv" version = "56.2.0" @@ -279,7 +277,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa9bf02705b5cf762b6f764c65f04ae9082c7cfc4e96e0c33548ee3f67012eb" dependencies = [ "arrow-array 56.2.0", - "arrow-cast", + "arrow-cast 56.2.0", "arrow-schema 56.2.0", "chrono", "csv", @@ -322,7 +320,7 @@ dependencies = [ "arrow-buffer 56.2.0", "arrow-data 56.2.0", "arrow-schema 56.2.0", - "arrow-select", + "arrow-select 56.2.0", "flatbuffers", ] @@ -334,7 +332,7 @@ checksum = "88cf36502b64a127dc659e3b305f1d993a544eab0d48cce704424e62074dc04b" dependencies = [ "arrow-array 56.2.0", "arrow-buffer 56.2.0", - "arrow-cast", + "arrow-cast 56.2.0", "arrow-data 56.2.0", "arrow-schema 56.2.0", "chrono", @@ -358,7 +356,20 @@ dependencies = [ "arrow-buffer 56.2.0", "arrow-data 56.2.0", "arrow-schema 56.2.0", - "arrow-select", + "arrow-select 56.2.0", +] + +[[package]] +name = "arrow-ord" +version = "57.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d8f1870e03d4cbed632959498bcc84083b5a24bded52905ae1695bd29da45b" +dependencies = [ + "arrow-array 57.3.0", + "arrow-buffer 57.3.0", + "arrow-data 57.3.0", + "arrow-schema 57.3.0", + "arrow-select 57.3.0", ] [[package]] @@ -406,6 +417,20 @@ dependencies = [ "num", ] +[[package]] +name = "arrow-select" +version = "57.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68bf3e3efbd1278f770d67e5dc410257300b161b93baedb3aae836144edcaf4b" +dependencies = [ + "ahash", + "arrow-array 57.3.0", + "arrow-buffer 57.3.0", + "arrow-data 57.3.0", + "arrow-schema 57.3.0", + "num-traits", +] + [[package]] name = "arrow-string" version = "56.2.0" @@ -416,7 +441,7 @@ dependencies = [ "arrow-buffer 56.2.0", "arrow-data 56.2.0", "arrow-schema 56.2.0", - "arrow-select", + "arrow-select 56.2.0", "memchr", "num", "regex", @@ -1079,7 +1104,7 @@ version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ - "anstream 1.0.0", + "anstream", "anstyle", "clap_lex", "strsim", @@ -1105,9 +1130,9 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "cmake" -version = "0.1.57" +version = "0.1.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" dependencies = [ "cc", ] @@ -1441,9 +1466,9 @@ dependencies = [ [[package]] name = "env_filter" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a1c3cc8e57274ec99de65301228b537f1e4eedc1b8e0f9411c6caac8ae7308f" +checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" dependencies = [ "log", "regex", @@ -1451,11 +1476,11 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.9" +version = "0.11.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2daee4ea451f429a58296525ddf28b45a3b64f1acf6587e2067437bb11e218d" +checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" dependencies = [ - "anstream 0.6.21", + "anstream", "anstyle", "env_filter", "jiff", @@ -1481,7 +1506,7 @@ dependencies = [ [[package]] name = "error_trace" version = "0.1.0" -source = "git+https://github.com/snowflakedb/universal-driver?rev=080422e05fbd727f68d7c494e564ec625e1375d6#080422e05fbd727f68d7c494e564ec625e1375d6" +source = "git+https://github.com/snowflakedb/universal-driver?rev=66a816ec1d1adda899bd2607f5c83e7dd5838ad5#66a816ec1d1adda899bd2607f5c83e7dd5838ad5" dependencies = [ "error_trace_derive", "snafu 0.8.9", @@ -1490,7 +1515,7 @@ dependencies = [ [[package]] name = "error_trace_derive" version = "0.1.0" -source = "git+https://github.com/snowflakedb/universal-driver?rev=080422e05fbd727f68d7c494e564ec625e1375d6#080422e05fbd727f68d7c494e564ec625e1375d6" +source = "git+https://github.com/snowflakedb/universal-driver?rev=66a816ec1d1adda899bd2607f5c83e7dd5838ad5#66a816ec1d1adda899bd2607f5c83e7dd5838ad5" dependencies = [ "quote", "syn 2.0.117", @@ -1598,6 +1623,21 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.32" @@ -1660,6 +1700,7 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -2193,9 +2234,9 @@ checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "iri-string" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +checksum = "d8e7418f59cc01c88316161279a7f665217ae316b388e58a0d10e29f54f1e5eb" dependencies = [ "memchr", "serde", @@ -2389,18 +2430,18 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" +checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08" dependencies = [ "libc", ] [[package]] name = "linux-keyutils" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "761e49ec5fd8a5a463f9b84e877c373d888935b71c6be78f3767fe2ae6bed18e" +checksum = "83270a18e9f90d0707c41e9f35efada77b64c0e6f3f1810e71c8368a864d5590" dependencies = [ "bitflags", "libc", @@ -2592,9 +2633,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" [[package]] name = "num-integer" @@ -3068,7 +3109,7 @@ dependencies = [ [[package]] name = "proto_generator" version = "0.1.0" -source = "git+https://github.com/snowflakedb/universal-driver?rev=080422e05fbd727f68d7c494e564ec625e1375d6#080422e05fbd727f68d7c494e564ec625e1375d6" +source = "git+https://github.com/snowflakedb/universal-driver?rev=66a816ec1d1adda899bd2607f5c83e7dd5838ad5#66a816ec1d1adda899bd2607f5c83e7dd5838ad5" dependencies = [ "clap", "env_logger", @@ -3085,7 +3126,7 @@ dependencies = [ [[package]] name = "proto_utils" version = "0.1.0" -source = "git+https://github.com/snowflakedb/universal-driver?rev=080422e05fbd727f68d7c494e564ec625e1375d6#080422e05fbd727f68d7c494e564ec625e1375d6" +source = "git+https://github.com/snowflakedb/universal-driver?rev=66a816ec1d1adda899bd2607f5c83e7dd5838ad5#66a816ec1d1adda899bd2607f5c83e7dd5838ad5" [[package]] name = "quinn" @@ -3387,7 +3428,7 @@ dependencies = [ "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.9", + "rustls-webpki 0.103.10", "subtle", "zeroize", ] @@ -3446,9 +3487,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.9" +version = "0.103.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" dependencies = [ "aws-lc-rs", "ring", @@ -3625,7 +3666,7 @@ dependencies = [ [[package]] name = "sf_core" version = "0.0.0" -source = "git+https://github.com/snowflakedb/universal-driver?rev=080422e05fbd727f68d7c494e564ec625e1375d6#080422e05fbd727f68d7c494e564ec625e1375d6" +source = "git+https://github.com/snowflakedb/universal-driver?rev=66a816ec1d1adda899bd2607f5c83e7dd5838ad5#66a816ec1d1adda899bd2607f5c83e7dd5838ad5" dependencies = [ "arrow", "arrow-ipc", @@ -3642,6 +3683,7 @@ dependencies = [ "dirs", "error_trace", "flate2", + "futures", "glob", "hex", "html-escape", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 115d329..06562a9 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -31,8 +31,9 @@ crate-type = ["cdylib", "rlib"] [dependencies] adbc_core = "0.22.0" adbc_ffi = "0.22.0" -sf_core = { git = "https://github.com/snowflakedb/universal-driver", subdirectory = "sf_core", rev = "080422e05fbd727f68d7c494e564ec625e1375d6" } +sf_core = { git = "https://github.com/snowflakedb/universal-driver", subdirectory = "sf_core", rev = "66a816ec1d1adda899bd2607f5c83e7dd5838ad5" } arrow-array = { version = "57.3.0", default-features = false, features = ["ffi"] } arrow-buffer = { version = "57.3.0", default-features = false } arrow-schema = { version = "57.3.0", default-features = false } +arrow-cast = { version = "57.3.0", default-features = false } tokio = { version = "1", features = ["rt-multi-thread"] } From f360a99fc391f469ce2ad929f3c91e472da8038a Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Thu, 26 Mar 2026 22:23:04 -0400 Subject: [PATCH 37/76] =?UTF-8?q?feat(statement):=20add=20ConvertingReader?= =?UTF-8?q?=20for=20Int8/Int16/Int32=20=E2=86=92=20Int64=20upcasting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rust/src/statement.rs | 809 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 795 insertions(+), 14 deletions(-) diff --git a/rust/src/statement.rs b/rust/src/statement.rs index 03fb714..8f2f39c 100644 --- a/rust/src/statement.rs +++ b/rust/src/statement.rs @@ -24,12 +24,13 @@ use std::sync::Arc; use adbc_core::{ + Optionable, PartitionedResult, error::{Error, Result, Status}, options::{OptionStatement, OptionValue}, }; -use arrow_array::{Array, RecordBatch, RecordBatchReader}; -use arrow_schema::{Schema}; +use arrow_array::{Array, ArrayRef, RecordBatch, RecordBatchReader}; +use arrow_schema::{DataType, Field, Schema, TimeUnit}; use sf_core::apis::database_driver_v1::Handle; use crate::driver::{Inner, TimestampPrecision}; @@ -220,11 +221,15 @@ impl Statement { } } - let schema = result_schema.unwrap_or_else(|| Arc::new(Schema::empty())); - Ok(Box::new(ConcatReader { - batches: all_batches.into_iter(), - schema, - })) + let schema = result_schema.unwrap_or_else(|| Arc::new(Schema::empty())); + Ok(Box::new(ConvertingReader::new( + ConcatReader { + batches: all_batches.into_iter(), + schema, + }, + self.use_high_precision, + self.timestamp_precision.time_unit(), + ))) } fn apply_query_tag(&self) -> Result<()> { @@ -309,9 +314,9 @@ impl adbc_core::Statement for Statement { // to ArrowArrayStreamReader. The C ABI layout is stable per the Arrow C Data Interface. let raw = Box::into_raw(result.stream) as *mut arrow_array::ffi_stream::FFI_ArrowArrayStream; - let reader = unsafe { arrow_array::ffi_stream::ArrowArrayStreamReader::from_raw(raw) } - .map_err(|e| Error::with_message_and_status(e.to_string(), Status::IO))?; - Ok(Box::new(reader)) + let reader = unsafe { arrow_array::ffi_stream::ArrowArrayStreamReader::from_raw(raw) } + .map_err(|e| Error::with_message_and_status(e.to_string(), Status::IO))?; + Ok(Box::new(ConvertingReader::new(reader, self.use_high_precision, self.timestamp_precision.time_unit()))) } fn execute_update(&mut self) -> Result> { @@ -413,9 +418,9 @@ impl adbc_core::Statement for Statement { Box::into_raw(result.stream) as *mut arrow_array::ffi_stream::FFI_ArrowArrayStream; let reader = unsafe { arrow_array::ffi_stream::ArrowArrayStreamReader::from_raw(raw) } .map_err(|e| Error::with_message_and_status(e.to_string(), Status::IO))?; - // .schema() calls get_schema on the FFI stream without consuming any record batches. - // Dropping the reader invokes the stream's release callback. - Ok(reader.schema().as_ref().clone()) + // .schema() calls get_schema on the FFI stream without consuming any record batches. + // Dropping the reader invokes the stream's release callback. + Ok(adjust_schema(&reader.schema(), self.use_high_precision, self.timestamp_precision.time_unit()).as_ref().clone()) } fn execute_partitions(&mut self) -> Result { @@ -473,6 +478,508 @@ impl RecordBatchReader for ConcatReader { } } +// ── Schema adjustment and type conversions ──────────────────────────────── + +fn scale_to_time_unit(scale: u32) -> TimeUnit { + match scale / 3 { + 0 => TimeUnit::Second, + 1 => TimeUnit::Millisecond, + 2 => TimeUnit::Microsecond, + _ => TimeUnit::Nanosecond, + } +} + +fn timestamp_target_unit(scale: i64, ts_unit: TimeUnit) -> TimeUnit { + let natural = scale_to_time_unit(scale as u32); + let natural_rank = time_unit_rank(natural); + let max_rank = time_unit_rank(ts_unit); + if natural_rank <= max_rank { + natural + } else { + ts_unit + } +} + +fn time_unit_rank(unit: TimeUnit) -> u32 { + match unit { + TimeUnit::Second => 0, + TimeUnit::Millisecond => 1, + TimeUnit::Microsecond => 2, + TimeUnit::Nanosecond => 3, + } +} + +pub(crate) fn adjust_schema( + schema: &Schema, + use_high_precision: bool, + ts_unit: TimeUnit, +) -> Arc { + let adjusted_fields: Vec = schema + .fields() + .iter() + .map(|f| { + let target = compute_target_type(f, use_high_precision, ts_unit); + Field::new(f.name(), target, f.is_nullable()) + }) + .collect(); + Arc::new(Schema::new_with_metadata( + adjusted_fields, + schema.metadata().clone(), + )) +} + +fn compute_target_type(field: &Field, use_high_precision: bool, ts_unit: TimeUnit) -> DataType { + let logical_type = field + .metadata() + .get("logicalType") + .map(|s| s.as_str()) + .unwrap_or(""); + let scale: i64 = field + .metadata() + .get("scale") + .and_then(|s| s.parse().ok()) + .unwrap_or(0); + + match logical_type { + "FIXED" => { + if scale == 0 || use_high_precision { + DataType::Int64 + } else { + DataType::Float64 + } + } + "TIME" => { + let unit = scale_to_time_unit(scale as u32); + if scale < 6 { + DataType::Time32(unit) + } else { + DataType::Time64(unit) + } + } + "TIMESTAMP_NTZ" => { + let unit = timestamp_target_unit(scale, ts_unit); + DataType::Timestamp(unit, None) + } + "REAL" => DataType::Float64, + "TIMESTAMP_LTZ" => { + let unit = timestamp_target_unit(scale, ts_unit); + DataType::Timestamp(unit, Some(Arc::from("UTC"))) + } + "TIMESTAMP_TZ" => { + let unit = timestamp_target_unit(scale, ts_unit); + DataType::Timestamp(unit, Some(Arc::from("UTC"))) + } + _ => match field.data_type() { + DataType::Int8 | DataType::Int16 | DataType::Int32 => DataType::Int64, + other => other.clone(), + }, + } +} + +#[cfg(test)] +pub(crate) fn adjust_schema_integers(schema: &Schema) -> Arc { + let adjusted_fields: Vec = schema + .fields() + .iter() + .map(|f| match f.data_type() { + DataType::Int8 | DataType::Int16 | DataType::Int32 => { + Field::new(f.name(), DataType::Int64, f.is_nullable()) + } + _ => f.as_ref().clone(), + }) + .collect(); + Arc::new(Schema::new_with_metadata( + adjusted_fields, + schema.metadata().clone(), + )) +} + +pub(crate) struct ConvertingReader { + inner: R, + schema: Arc, + use_high_precision: bool, + ts_unit: TimeUnit, + logical_types: Vec, + scales: Vec, +} + +impl ConvertingReader { + pub(crate) fn new(inner: R, use_high_precision: bool, ts_unit: TimeUnit) -> Self { + let orig_schema = inner.schema(); + let logical_types: Vec = orig_schema + .fields() + .iter() + .map(|f| { + f.metadata() + .get("logicalType") + .cloned() + .unwrap_or_default() + }) + .collect(); + let scales: Vec = orig_schema + .fields() + .iter() + .map(|f| { + f.metadata() + .get("scale") + .and_then(|s| s.parse().ok()) + .unwrap_or(0) + }) + .collect(); + let schema = adjust_schema(&orig_schema, use_high_precision, ts_unit); + Self { + inner, + schema, + use_high_precision, + ts_unit, + logical_types, + scales, + } + } + + fn convert_column( + col: &ArrayRef, + logical_type: &str, + scale: i64, + target_type: &DataType, + use_high_precision: bool, + ts_unit: TimeUnit, + check_overflow: bool, + ) -> std::result::Result { + match logical_type { + "FIXED" => { + if scale == 0 { + match col.data_type() { + DataType::Int64 => Ok(col.clone()), + _ => arrow_cast::cast(col.as_ref(), &DataType::Int64), + } + } else if use_high_precision { + Ok(col.clone()) + } else { + // Cast to Float64, then divide by 10^scale to restore decimal value + let casted = arrow_cast::cast(col.as_ref(), &DataType::Float64)?; + let divisor = 10f64.powi(scale as i32); + let float_arr = casted + .as_any() + .downcast_ref::() + .ok_or_else(|| { + arrow_schema::ArrowError::CastError( + "expected Float64Array after cast".into(), + ) + })?; + let divided: arrow_array::Float64Array = + float_arr.iter().map(|v| v.map(|x| x / divisor)).collect(); + Ok(Arc::new(divided) as ArrayRef) + } + } + "TIME" => arrow_cast::cast(col.as_ref(), target_type), + "REAL" => arrow_cast::cast(col.as_ref(), &DataType::Float64), + "TIMESTAMP_NTZ" => convert_timestamp_ntz(col, scale, ts_unit, check_overflow, None), + "TIMESTAMP_LTZ" => convert_timestamp_ntz( + col, + scale, + ts_unit, + check_overflow, + Some(Arc::from("UTC")), + ), + "TIMESTAMP_TZ" => convert_timestamp_tz(col, scale, ts_unit, check_overflow), + _ => match col.data_type() { + DataType::Int8 | DataType::Int16 | DataType::Int32 => { + arrow_cast::cast(col.as_ref(), &DataType::Int64) + } + _ => Ok(col.clone()), + }, + } + } +} + +fn convert_timestamp_ntz( + col: &ArrayRef, + scale: i64, + ts_unit: TimeUnit, + check_overflow: bool, + tz_str: Option>, +) -> std::result::Result { + let unit = timestamp_target_unit(scale, ts_unit); + let target = DataType::Timestamp(unit, tz_str); + + match col.data_type() { + DataType::Int64 => arrow_cast::cast(col.as_ref(), &target), + DataType::Struct(_) => { + use arrow_array::{Int32Array, Int64Array, StructArray}; + let struct_arr = col + .as_any() + .downcast_ref::() + .ok_or_else(|| arrow_schema::ArrowError::CastError("expected StructArray".into()))?; + let epoch = struct_arr + .column(0) + .as_any() + .downcast_ref::() + .ok_or_else(|| { + arrow_schema::ArrowError::CastError("expected Int64 epoch".into()) + })?; + let fraction = struct_arr + .column(1) + .as_any() + .downcast_ref::() + .ok_or_else(|| { + arrow_schema::ArrowError::CastError("expected Int32 fraction".into()) + })?; + + build_timestamp_from_epoch_fraction(epoch, fraction, struct_arr, scale, unit, check_overflow, target) + } + _ => arrow_cast::cast(col.as_ref(), &target), + } +} + +fn convert_timestamp_tz( + col: &ArrayRef, + scale: i64, + ts_unit: TimeUnit, + check_overflow: bool, +) -> std::result::Result { + use arrow_array::{Int32Array, Int64Array, StructArray}; + + let unit = timestamp_target_unit(scale, ts_unit); + let target = DataType::Timestamp(unit, Some(Arc::from("UTC"))); + + let struct_arr = col + .as_any() + .downcast_ref::() + .ok_or_else(|| arrow_schema::ArrowError::CastError("expected StructArray for TIMESTAMP_TZ".into()))?; + + let num_fields = struct_arr.num_columns(); + let epoch = struct_arr + .column(0) + .as_any() + .downcast_ref::() + .ok_or_else(|| arrow_schema::ArrowError::CastError("expected Int64 epoch".into()))?; + + if num_fields == 2 { + let tzoffset = struct_arr + .column(1) + .as_any() + .downcast_ref::() + .ok_or_else(|| arrow_schema::ArrowError::CastError("expected Int32 timezone".into()))?; + + build_timestamp_tz_2field(epoch, tzoffset, struct_arr, scale, unit, check_overflow, target) + } else { + let fraction = struct_arr + .column(1) + .as_any() + .downcast_ref::() + .ok_or_else(|| arrow_schema::ArrowError::CastError("expected Int32 fraction".into()))?; + let tzoffset = struct_arr + .column(2) + .as_any() + .downcast_ref::() + .ok_or_else(|| arrow_schema::ArrowError::CastError("expected Int32 timezone".into()))?; + + build_timestamp_tz_3field(epoch, fraction, tzoffset, struct_arr, scale, unit, check_overflow, target) + } +} + +fn ns_to_unit(ns: i128, unit: TimeUnit) -> i64 { + let divisor: i128 = match unit { + TimeUnit::Second => 1_000_000_000, + TimeUnit::Millisecond => 1_000_000, + TimeUnit::Microsecond => 1_000, + TimeUnit::Nanosecond => 1, + }; + (ns / divisor) as i64 +} + +fn check_ns_overflow(ns: i128) -> std::result::Result<(), arrow_schema::ArrowError> { + if ns > i64::MAX as i128 || ns < i64::MIN as i128 { + Err(arrow_schema::ArrowError::CastError(format!( + "timestamp value {ns} nanoseconds overflows i64" + ))) + } else { + Ok(()) + } +} + +fn build_timestamp_from_epoch_fraction( + epoch: &arrow_array::Int64Array, + fraction: &arrow_array::Int32Array, + struct_arr: &arrow_array::StructArray, + scale: i64, + unit: TimeUnit, + check_overflow: bool, + target: DataType, +) -> std::result::Result { + use arrow_array::builder::PrimitiveBuilder; + use arrow_array::types::TimestampNanosecondType; + + let len = epoch.len(); + let frac_to_ns: i128 = if scale <= 9 { + 10i128.pow((9 - scale) as u32) + } else { + 1 + }; + + let mut builder = PrimitiveBuilder::::with_capacity(len); + for i in 0..len { + if struct_arr.is_null(i) { + builder.append_null(); + } else { + let ns: i128 = if epoch.value(i) >= 0 { + epoch.value(i) as i128 * 1_000_000_000 + + fraction.value(i) as i128 * frac_to_ns + } else { + epoch.value(i) as i128 * 1_000_000_000 + - fraction.value(i) as i128 * frac_to_ns + }; + if check_overflow { + check_ns_overflow(ns)?; + } + let val = ns_to_unit(ns, unit); + builder.append_value(val); + } + } + let ns_arr = builder.finish(); + let intermediate: Arc = Arc::new(ns_arr); + arrow_cast::cast(intermediate.as_ref(), &target) +} + +fn build_timestamp_tz_2field( + epoch: &arrow_array::Int64Array, + tzoffset: &arrow_array::Int32Array, + struct_arr: &arrow_array::StructArray, + scale: i64, + unit: TimeUnit, + check_overflow: bool, + target: DataType, +) -> std::result::Result { + use arrow_array::builder::PrimitiveBuilder; + use arrow_array::types::TimestampNanosecondType; + + let len = epoch.len(); + let mut builder = PrimitiveBuilder::::with_capacity(len); + + for i in 0..len { + if struct_arr.is_null(i) { + builder.append_null(); + } else { + let tz_offset_minutes: i128 = (tzoffset.value(i) as i128) - 1440; + let tz_offset_ns: i128 = tz_offset_minutes * 60 * 1_000_000_000; + + let epoch_ns: i128 = match scale { + 0..=2 => epoch.value(i) as i128 * 1_000_000_000, + 3..=5 => epoch.value(i) as i128 * 1_000_000, + 6..=8 => epoch.value(i) as i128 * 1_000, + 9 => epoch.value(i) as i128, + _ => epoch.value(i) as i128 * 1_000_000_000, + }; + + let utc_ns = epoch_ns - tz_offset_ns; + if check_overflow { + check_ns_overflow(utc_ns)?; + } + builder.append_value(ns_to_unit(utc_ns, unit)); + } + } + let ns_arr = builder.finish(); + let intermediate: Arc = Arc::new(ns_arr); + arrow_cast::cast(intermediate.as_ref(), &target) +} + +#[allow(clippy::too_many_arguments)] +fn build_timestamp_tz_3field( + epoch: &arrow_array::Int64Array, + fraction: &arrow_array::Int32Array, + tzoffset: &arrow_array::Int32Array, + struct_arr: &arrow_array::StructArray, + scale: i64, + unit: TimeUnit, + check_overflow: bool, + target: DataType, +) -> std::result::Result { + use arrow_array::builder::PrimitiveBuilder; + use arrow_array::types::TimestampNanosecondType; + + let len = epoch.len(); + let frac_to_ns: i128 = if scale <= 9 { + 10i128.pow((9 - scale) as u32) + } else { + 1 + }; + let mut builder = PrimitiveBuilder::::with_capacity(len); + + for i in 0..len { + if struct_arr.is_null(i) { + builder.append_null(); + } else { + let tz_offset_minutes: i128 = (tzoffset.value(i) as i128) - 1440; + let tz_offset_ns: i128 = tz_offset_minutes * 60 * 1_000_000_000; + + let epoch_ns: i128 = if epoch.value(i) >= 0 { + epoch.value(i) as i128 * 1_000_000_000 + + fraction.value(i) as i128 * frac_to_ns + } else { + epoch.value(i) as i128 * 1_000_000_000 + - fraction.value(i) as i128 * frac_to_ns + }; + + let utc_ns = epoch_ns - tz_offset_ns; + if check_overflow { + check_ns_overflow(utc_ns)?; + } + builder.append_value(ns_to_unit(utc_ns, unit)); + } + } + let ns_arr = builder.finish(); + let intermediate: Arc = Arc::new(ns_arr); + arrow_cast::cast(intermediate.as_ref(), &target) +} + +impl Iterator for ConvertingReader { + type Item = std::result::Result; + + fn next(&mut self) -> Option { + let batch = match self.inner.next()? { + Ok(b) => b, + Err(e) => return Some(Err(e)), + }; + + let check_overflow = false; + + let adjusted_columns: std::result::Result, arrow_schema::ArrowError> = batch + .columns() + .iter() + .enumerate() + .map(|(i, col)| { + let logical_type = &self.logical_types[i]; + let scale = self.scales[i]; + let target_type = self.schema.field(i).data_type(); + ConvertingReader::::convert_column( + col, + logical_type, + scale, + target_type, + self.use_high_precision, + self.ts_unit, + check_overflow, + ) + }) + .collect(); + + Some(RecordBatch::try_new( + self.schema.clone(), + match adjusted_columns { + Ok(cols) => cols, + Err(e) => return Some(Err(e)), + }, + )) + } +} + +impl RecordBatchReader for ConvertingReader { + fn schema(&self) -> Arc { + self.schema.clone() + } +} + // ── Parameter substitution ──────────────────────────────────────────────────── /// Replaces each `?` placeholder in `query` with the SQL literal value of the @@ -812,4 +1319,278 @@ mod tests { // Verify conn_handle is present on the struct (compile-time check) let _ = stmt.conn_handle; } -} + + #[test] + fn test_adjust_schema_int8_to_int64() { + let schema = Schema::new(vec![Field::new("a", DataType::Int8, false)]); + let result = adjust_schema_integers(&schema); + assert_eq!(result.field(0).data_type(), &DataType::Int64); + } + + #[test] + fn test_adjust_schema_int16_to_int64() { + let schema = Schema::new(vec![Field::new("b", DataType::Int16, true)]); + let result = adjust_schema_integers(&schema); + assert_eq!(result.field(0).data_type(), &DataType::Int64); + } + + #[test] + fn test_adjust_schema_int32_to_int64() { + let schema = Schema::new(vec![Field::new("c", DataType::Int32, false)]); + let result = adjust_schema_integers(&schema); + assert_eq!(result.field(0).data_type(), &DataType::Int64); + } + + #[test] + fn test_adjust_schema_int64_passthrough() { + let schema = Schema::new(vec![Field::new("d", DataType::Int64, false)]); + let result = adjust_schema_integers(&schema); + assert_eq!(result.field(0).data_type(), &DataType::Int64); + } + + #[test] + fn test_adjust_schema_mixed() { + use arrow_schema::DataType; + let schema = Schema::new(vec![ + Field::new("i8", DataType::Int8, false), + Field::new("s", DataType::Utf8, false), + Field::new("i32", DataType::Int32, true), + Field::new("f64", DataType::Float64, false), + ]); + let result = adjust_schema_integers(&schema); + assert_eq!(result.field(0).data_type(), &DataType::Int64); + assert_eq!(result.field(1).data_type(), &DataType::Utf8); + assert_eq!(result.field(2).data_type(), &DataType::Int64); + assert_eq!(result.field(3).data_type(), &DataType::Float64); + } + + #[test] + fn test_converting_reader_int8_values() { + use arrow_array::Int8Array; + let schema = Arc::new(Schema::new(vec![Field::new("v", DataType::Int8, false)])); + let batch = RecordBatch::try_new( + schema.clone(), + vec![Arc::new(Int8Array::from(vec![1i8, 2, 3]))], + ) + .unwrap(); + let reader = ConcatReader { + batches: vec![batch].into_iter(), + schema, + }; + let mut cr = ConvertingReader::new(reader, true, TimeUnit::Nanosecond); + let out = cr.next().unwrap().unwrap(); + assert_eq!(out.schema().field(0).data_type(), &DataType::Int64); + let col = out + .column(0) + .as_any() + .downcast_ref::() + .unwrap(); + assert_eq!(col.values(), &[1i64, 2, 3]); + } + + #[test] + fn test_converting_reader_null_values() { + use arrow_array::Int16Array; + let schema = Arc::new(Schema::new(vec![Field::new("v", DataType::Int16, true)])); + let arr = Int16Array::from(vec![Some(10i16), None, Some(30)]); + let batch = + RecordBatch::try_new(schema.clone(), vec![Arc::new(arr)]).unwrap(); + let reader = ConcatReader { + batches: vec![batch].into_iter(), + schema, + }; + let mut cr = ConvertingReader::new(reader, true, TimeUnit::Nanosecond); + let out = cr.next().unwrap().unwrap(); + let col = out + .column(0) + .as_any() + .downcast_ref::() + .unwrap(); + assert_eq!(col.value(0), 10i64); + assert!(col.is_null(1)); + assert_eq!(col.value(2), 30i64); + } + + #[test] + fn test_converting_reader_empty_batch() { + let schema = Arc::new(Schema::new(vec![Field::new("v", DataType::Int32, false)])); + let batch = RecordBatch::new_empty(schema.clone()); + let reader = ConcatReader { + batches: vec![batch].into_iter(), + schema, + }; + let mut cr = ConvertingReader::new(reader, true, TimeUnit::Nanosecond); + let out = cr.next().unwrap().unwrap(); + assert_eq!(out.schema().field(0).data_type(), &DataType::Int64); + assert_eq!(out.num_rows(), 0); + } + + #[test] + fn test_converting_reader_multiple_batches_different_widths() { + use arrow_array::{Int32Array, Int8Array}; + struct TwoBatchReader { + batches: std::vec::IntoIter, + schema: Arc, + } + impl Iterator for TwoBatchReader { + type Item = std::result::Result; + fn next(&mut self) -> Option { + self.batches.next().map(Ok) + } + } + impl RecordBatchReader for TwoBatchReader { + fn schema(&self) -> Arc { + self.schema.clone() + } + } + + let declared_schema = + Arc::new(Schema::new(vec![Field::new("v", DataType::Int64, false)])); + + let schema_i8 = Arc::new(Schema::new(vec![Field::new("v", DataType::Int8, false)])); + let schema_i32 = Arc::new(Schema::new(vec![Field::new("v", DataType::Int32, false)])); + let batch1 = RecordBatch::try_new( + schema_i8, + vec![Arc::new(Int8Array::from(vec![1i8, 2]))], + ) + .unwrap(); + let batch2 = RecordBatch::try_new( + schema_i32, + vec![Arc::new(Int32Array::from(vec![100i32, 200]))], + ) + .unwrap(); + + let reader = TwoBatchReader { + batches: vec![batch1, batch2].into_iter(), + schema: declared_schema, + }; + let mut cr = ConvertingReader::new(reader, true, TimeUnit::Nanosecond); + + let out1 = cr.next().unwrap().unwrap(); + assert_eq!(out1.column(0).data_type(), &DataType::Int64); + let col1 = out1 + .column(0) + .as_any() + .downcast_ref::() + .unwrap(); + assert_eq!(col1.values(), &[1i64, 2]); + + let out2 = cr.next().unwrap().unwrap(); + assert_eq!(out2.column(0).data_type(), &DataType::Int64); + let col2 = out2 + .column(0) + .as_any() + .downcast_ref::() + .unwrap(); + assert_eq!(col2.values(), &[100i64, 200]); + } + fn make_field_with_meta(name: &str, dt: DataType, logical_type: &str, scale: &str) -> Field { + let mut md = std::collections::HashMap::new(); + md.insert("logicalType".to_string(), logical_type.to_string()); + md.insert("scale".to_string(), scale.to_string()); + Field::new(name, dt, true).with_metadata(md) + } + + #[test] + fn test_adjust_schema_time_scale3_is_time32_millisecond() { + let f = make_field_with_meta("t", DataType::Int32, "TIME", "3"); + let schema = Schema::new(vec![f]); + let result = adjust_schema(&schema, false, TimeUnit::Nanosecond); + assert_eq!(result.field(0).data_type(), &DataType::Time32(TimeUnit::Millisecond)); + } + + #[test] + fn test_adjust_schema_time_scale9_is_time64_nanosecond() { + let f = make_field_with_meta("t", DataType::Int64, "TIME", "9"); + let schema = Schema::new(vec![f]); + let result = adjust_schema(&schema, false, TimeUnit::Nanosecond); + assert_eq!(result.field(0).data_type(), &DataType::Time64(TimeUnit::Nanosecond)); + } + + #[test] + fn test_adjust_schema_fixed_scale2_is_float64() { + let f = make_field_with_meta("x", DataType::Int64, "FIXED", "2"); + let schema = Schema::new(vec![f]); + let result = adjust_schema(&schema, false, TimeUnit::Nanosecond); + assert_eq!(result.field(0).data_type(), &DataType::Float64); + } + + #[test] + fn test_adjust_schema_fixed_scale2_high_precision_stays_int64() { + let f = make_field_with_meta("x", DataType::Int64, "FIXED", "2"); + let schema = Schema::new(vec![f]); + let result = adjust_schema(&schema, true, TimeUnit::Nanosecond); + assert_eq!(result.field(0).data_type(), &DataType::Int64); + } + + #[test] + fn test_adjust_schema_timestamp_ntz_int64_to_timestamp_ns() { + let f = make_field_with_meta("ts", DataType::Int64, "TIMESTAMP_NTZ", "9"); + let schema = Schema::new(vec![f]); + let result = adjust_schema(&schema, false, TimeUnit::Nanosecond); + assert_eq!(result.field(0).data_type(), &DataType::Timestamp(TimeUnit::Nanosecond, None)); + } + + #[test] + fn test_adjust_schema_timestamp_ltz_is_utc() { + let f = make_field_with_meta("ts", DataType::Int64, "TIMESTAMP_LTZ", "6"); + let schema = Schema::new(vec![f]); + let result = adjust_schema(&schema, false, TimeUnit::Microsecond); + assert_eq!( + result.field(0).data_type(), + &DataType::Timestamp(TimeUnit::Microsecond, Some(Arc::from("UTC"))) + ); + } + + #[test] + fn test_converting_reader_fixed_scale2_produces_float64() { + let f = make_field_with_meta("x", DataType::Int64, "FIXED", "2"); + let schema = Arc::new(Schema::new(vec![f])); + let batch = RecordBatch::try_new( + schema.clone(), + vec![Arc::new(arrow_array::Int64Array::from(vec![12345i64, 255]))], + ) + .unwrap(); + let reader = ConcatReader { batches: vec![batch].into_iter(), schema }; + let mut cr = ConvertingReader::new(reader, false, TimeUnit::Nanosecond); + let out = cr.next().unwrap().unwrap(); + assert_eq!(out.schema().field(0).data_type(), &DataType::Float64); + let col = out.column(0).as_any().downcast_ref::().unwrap(); + assert!((col.value(0) - 123.45).abs() < 1e-9); + assert!((col.value(1) - 2.55).abs() < 1e-9); + } + + #[test] + fn test_converting_reader_timestamp_ntz_int64_cast() { + let f = make_field_with_meta("ts", DataType::Int64, "TIMESTAMP_NTZ", "9"); + let schema = Arc::new(Schema::new(vec![f])); + let epoch_ns: i64 = 1_000_000_000; + let batch = RecordBatch::try_new( + schema.clone(), + vec![Arc::new(arrow_array::Int64Array::from(vec![epoch_ns]))], + ) + .unwrap(); + let reader = ConcatReader { batches: vec![batch].into_iter(), schema }; + let mut cr = ConvertingReader::new(reader, false, TimeUnit::Nanosecond); + let out = cr.next().unwrap().unwrap(); + assert_eq!( + out.schema().field(0).data_type(), + &DataType::Timestamp(TimeUnit::Nanosecond, None) + ); + let col = out + .column(0) + .as_any() + .downcast_ref::() + .unwrap(); + assert_eq!(col.value(0), epoch_ns); + } + + #[test] + fn test_adjust_schema_timestamp_ntz_capped_by_ts_unit() { + let f = make_field_with_meta("ts", DataType::Int64, "TIMESTAMP_NTZ", "9"); + let schema = Schema::new(vec![f]); + let result = adjust_schema(&schema, false, TimeUnit::Microsecond); + assert_eq!(result.field(0).data_type(), &DataType::Timestamp(TimeUnit::Microsecond, None)); + } + +} \ No newline at end of file From c77fc999db0641d00caaf3ff3b6c0886948a8c38 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Thu, 26 Mar 2026 22:23:20 -0400 Subject: [PATCH 38/76] fix(deps): handle new ApiError variants from sf_core rev bump and minor refactors --- rust/src/error.rs | 4 ++++ rust/src/get_objects.rs | 9 +++------ rust/src/ingest.rs | 6 ++---- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/rust/src/error.rs b/rust/src/error.rs index 155fb02..94dbf0c 100644 --- a/rust/src/error.rs +++ b/rust/src/error.rs @@ -42,6 +42,10 @@ pub(crate) fn api_error_to_adbc_error(err: ApiError) -> Error { ApiError::Statement { .. } => Status::IO, ApiError::RuntimeCreation { .. } => Status::IO, ApiError::GenericError { .. } => Status::IO, + ApiError::TokenCacheInitialization { .. } => Status::IO, + ApiError::ArrowParsing { .. } => Status::IO, + ApiError::ChunkFetch { .. } => Status::IO, + ApiError::Base64Decoding { .. } => Status::IO, }; Error::with_message_and_status(err.to_string(), status) } diff --git a/rust/src/get_objects.rs b/rust/src/get_objects.rs index 353380a..b4cdc20 100644 --- a/rust/src/get_objects.rs +++ b/rust/src/get_objects.rs @@ -221,7 +221,7 @@ pub(crate) fn execute_get_objects( table_type_filter, column_name_filter, )?; - let batch = build_batch(&entries, &depth)?; + let batch = build_batch(&entries, depth)?; Ok(Box::new(SingleBatchReader::new(batch))) } @@ -382,8 +382,8 @@ fn collect( continue; }; let key = (cat.clone(), sch.clone()); - if let Some(tables) = tables_by_key.get_mut(&key) { - if let Some(table) = tables.iter_mut().find(|t| &t.name == tbl) { + if let Some(tables) = tables_by_key.get_mut(&key) + && let Some(table) = tables.iter_mut().find(|t| &t.name == tbl) { let nullable_str = row[7].clone(); let nullable_int = nullable_str.as_deref().map(|s| { if s.eq_ignore_ascii_case("YES") { @@ -392,8 +392,6 @@ fn collect( 0 } }); - // Compute 1-based ordinal from insertion order (SQL is ordered by - // ordinal_position so this matches the database's sort order). let ordinal = { let c = col_counter .entry((cat.clone(), sch.clone(), tbl.clone())) @@ -415,7 +413,6 @@ fn collect( xdbc_datetime_sub: row[12].as_deref().and_then(|s| s.parse().ok()), }); } - } } Ok(assemble(catalog_names, schemas_by_cat, tables_by_key)) diff --git a/rust/src/ingest.rs b/rust/src/ingest.rs index 6f32020..c11db2e 100644 --- a/rust/src/ingest.rs +++ b/rust/src/ingest.rs @@ -405,12 +405,10 @@ fn value_to_sql(arr: &dyn Array, row: usize, dt: &DataType) -> Result { }); } - // DECIMAL - if let Some(a) = arr.as_any().downcast_ref::() { - if let DataType::Decimal128(_, scale) = dt { + if let Some(a) = arr.as_any().downcast_ref::() + && let DataType::Decimal128(_, scale) = dt { return Ok(decimal128_to_str(a.value(row), *scale)); } - } Err(Error::with_message_and_status( format!("unsupported ingest value type: {dt:?}"), From fd7cf3805c28d9d595b2d583578e31be5433e394 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Fri, 27 Mar 2026 12:06:15 -0400 Subject: [PATCH 39/76] fix(connection): parse timestamp scale from Snowflake type string for correct Arrow precision Add chrono-tz feature to arrow-array so named timezone strings like 'UTC' are accepted by pyarrow. Fix snowflake_type_to_arrow() to extract the scale from type strings such as TIMESTAMP_NTZ(6) and map it to the natural Arrow TimeUnit (s/ms/us/ns), then take min(natural, ts_unit) to match the compute_target_type() logic in statement.rs. Add 4 unit tests covering the new precision-capping behaviour. --- rust/Cargo.lock | 35 ++++++++++++++++++++++ rust/Cargo.toml | 2 +- rust/src/connection.rs | 68 ++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 102 insertions(+), 3 deletions(-) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index b70247e..a40858a 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -199,6 +199,7 @@ dependencies = [ "arrow-data 57.3.0", "arrow-schema 57.3.0", "chrono", + "chrono-tz", "half", "hashbrown 0.16.1", "num-complex", @@ -1088,6 +1089,16 @@ dependencies = [ "windows-link", ] +[[package]] +name = "chrono-tz" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3" +dependencies = [ + "chrono", + "phf", +] + [[package]] name = "clap" version = "4.6.0" @@ -2893,6 +2904,24 @@ dependencies = [ "indexmap", ] +[[package]] +name = "phf" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_shared" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" version = "1.1.11" @@ -3807,6 +3836,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + [[package]] name = "slab" version = "0.4.12" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 06562a9..2695e0d 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -32,7 +32,7 @@ crate-type = ["cdylib", "rlib"] adbc_core = "0.22.0" adbc_ffi = "0.22.0" sf_core = { git = "https://github.com/snowflakedb/universal-driver", subdirectory = "sf_core", rev = "66a816ec1d1adda899bd2607f5c83e7dd5838ad5" } -arrow-array = { version = "57.3.0", default-features = false, features = ["ffi"] } +arrow-array = { version = "57.3.0", default-features = false, features = ["ffi", "chrono-tz"] } arrow-buffer = { version = "57.3.0", default-features = false } arrow-schema = { version = "57.3.0", default-features = false } arrow-cast = { version = "57.3.0", default-features = false } diff --git a/rust/src/connection.rs b/rust/src/connection.rs index aabee1d..43747d4 100644 --- a/rust/src/connection.rs +++ b/rust/src/connection.rs @@ -661,8 +661,36 @@ fn snowflake_type_to_arrow( DataType::Int64 } } - "TIMESTAMP" | "TIMESTAMP_NTZ" | "DATETIME" => DataType::Timestamp(ts_unit, None), - "TIMESTAMP_LTZ" | "TIMESTAMP_TZ" => DataType::Timestamp(ts_unit, Some("UTC".into())), + "TIMESTAMP" | "TIMESTAMP_NTZ" | "DATETIME" => { + let scale = type_str + .find('(') + .and_then(|s| type_str.rfind(')').map(|e| &type_str[s + 1..e])) + .and_then(|s| s.trim().parse::().ok()) + .unwrap_or(9); + let natural = match scale / 3 { + 0 => arrow_schema::TimeUnit::Second, + 1 => arrow_schema::TimeUnit::Millisecond, + 2 => arrow_schema::TimeUnit::Microsecond, + _ => arrow_schema::TimeUnit::Nanosecond, + }; + let unit = if (natural as u32) <= (ts_unit as u32) { natural } else { ts_unit }; + DataType::Timestamp(unit, None) + } + "TIMESTAMP_LTZ" | "TIMESTAMP_TZ" => { + let scale = type_str + .find('(') + .and_then(|s| type_str.rfind(')').map(|e| &type_str[s + 1..e])) + .and_then(|s| s.trim().parse::().ok()) + .unwrap_or(9); + let natural = match scale / 3 { + 0 => arrow_schema::TimeUnit::Second, + 1 => arrow_schema::TimeUnit::Millisecond, + 2 => arrow_schema::TimeUnit::Microsecond, + _ => arrow_schema::TimeUnit::Nanosecond, + }; + let unit = if (natural as u32) <= (ts_unit as u32) { natural } else { ts_unit }; + DataType::Timestamp(unit, Some("UTC".into())) + } _ => DataType::Utf8, } } @@ -754,6 +782,42 @@ mod tests { ); } + #[test] + fn snowflake_type_timestamp_ntz_scale6_with_ns_unit_returns_us() { + assert_eq!( + snowflake_type_to_arrow( + "TIMESTAMP_NTZ(6)", + true, + arrow_schema::TimeUnit::Nanosecond + ), + DataType::Timestamp(arrow_schema::TimeUnit::Microsecond, None) + ); + } + + #[test] + fn snowflake_type_timestamp_ntz_scale9_capped_by_us_unit() { + assert_eq!( + snowflake_type_to_arrow( + "TIMESTAMP_NTZ(9)", + true, + arrow_schema::TimeUnit::Microsecond + ), + DataType::Timestamp(arrow_schema::TimeUnit::Microsecond, None) + ); + } + + #[test] + fn snowflake_type_timestamp_ltz_scale6_with_ns_unit_returns_us() { + assert_eq!( + snowflake_type_to_arrow( + "TIMESTAMP_LTZ(6)", + true, + arrow_schema::TimeUnit::Nanosecond + ), + DataType::Timestamp(arrow_schema::TimeUnit::Microsecond, Some("UTC".into())) + ); + } + #[test] fn get_table_types_returns_table_and_view() { use adbc_core::Connection as _; From 527f04780a0168c1d85de90514c4deb95d08c557 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Fri, 27 Mar 2026 12:34:16 -0400 Subject: [PATCH 40/76] =?UTF-8?q?fix(statement):=20clear=20bound=5Fbatches?= =?UTF-8?q?=20after=20ingest/set=5Fsql=5Fquery;=20fix=20Int64=E2=86=92Time?= =?UTF-8?q?stamp=20unit=20mismatch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After adbc_ingest, bound_batches was not cleared, causing execute_query to run the SELECT once per bound row (N× duplicate results). Fix by clearing bound_batches in set_sql_query, execute_update ingest path, and execute() ingest path — matching Go driver behaviour. Also fix convert_timestamp_ntz: when sf_core returns TIMESTAMP_LTZ as Int64 (nanoseconds for scale=9), arrow_cast::cast(Int64→Timestamp(us)) was treating the value as microseconds. Fix by first casting to Timestamp(natural_unit) then rescaling to the target unit. --- rust/src/statement.rs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/rust/src/statement.rs b/rust/src/statement.rs index 8f2f39c..608cc88 100644 --- a/rust/src/statement.rs +++ b/rust/src/statement.rs @@ -280,6 +280,7 @@ impl adbc_core::Statement for Statement { if self.target_table.is_some() { // Ingest via execute() — run the ingest and return an empty reader. crate::ingest::execute_ingest(self)?; + self.bound_batches.clear(); let batch = arrow_array::RecordBatch::new_empty(Arc::new(arrow_schema::Schema::empty())); return Ok(Box::new(crate::connection::SingleBatchReader::new(batch))); @@ -321,7 +322,9 @@ impl adbc_core::Statement for Statement { fn execute_update(&mut self) -> Result> { if self.target_table.is_some() { - return crate::ingest::execute_ingest(self); + let result = crate::ingest::execute_ingest(self); + self.bound_batches.clear(); + return result; } let query = self.query.clone().ok_or_else(|| { Error::with_message_and_status("cannot execute without a query", Status::InvalidState) @@ -444,6 +447,7 @@ impl adbc_core::Statement for Statement { fn set_sql_query(&mut self, query: impl AsRef) -> Result<()> { self.query = Some(query.as_ref().to_string()); self.target_table = None; + self.bound_batches.clear(); Ok(()) } @@ -701,10 +705,19 @@ fn convert_timestamp_ntz( tz_str: Option>, ) -> std::result::Result { let unit = timestamp_target_unit(scale, ts_unit); - let target = DataType::Timestamp(unit, tz_str); + let target = DataType::Timestamp(unit, tz_str.clone()); match col.data_type() { - DataType::Int64 => arrow_cast::cast(col.as_ref(), &target), + DataType::Int64 => { + let natural_unit = scale_to_time_unit(scale as u32); + if natural_unit == unit { + arrow_cast::cast(col.as_ref(), &target) + } else { + let natural_target = DataType::Timestamp(natural_unit, tz_str); + let intermediate = arrow_cast::cast(col.as_ref(), &natural_target)?; + arrow_cast::cast(intermediate.as_ref(), &target) + } + } DataType::Struct(_) => { use arrow_array::{Int32Array, Int64Array, StructArray}; let struct_arr = col From 75514a2a8c0eceafe089c3389ab202d5f283b42f Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Fri, 27 Mar 2026 12:47:43 -0400 Subject: [PATCH 41/76] fix(statement): store raw nanoseconds in timestamp builders, let arrow_cast rescale build_timestamp_from_epoch_fraction, build_timestamp_tz_2field, and build_timestamp_tz_3field were calling ns_to_unit(ns, unit) to pre-convert to the target unit, then storing the result in a TimestampNanosecond builder. arrow_cast::cast(TimestampNanosecond -> Timestamp(us)) then divided by 1000 again, producing values 1000x too small (wrong epoch dates). Fix: store raw nanoseconds (ns as i64) in the builder and let arrow_cast handle the single unit conversion from nanoseconds to the target unit. --- rust/src/statement.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/rust/src/statement.rs b/rust/src/statement.rs index 608cc88..5c581f1 100644 --- a/rust/src/statement.rs +++ b/rust/src/statement.rs @@ -792,6 +792,7 @@ fn convert_timestamp_tz( } } +#[allow(dead_code)] fn ns_to_unit(ns: i128, unit: TimeUnit) -> i64 { let divisor: i128 = match unit { TimeUnit::Second => 1_000_000_000, @@ -817,7 +818,7 @@ fn build_timestamp_from_epoch_fraction( fraction: &arrow_array::Int32Array, struct_arr: &arrow_array::StructArray, scale: i64, - unit: TimeUnit, + _unit: TimeUnit, check_overflow: bool, target: DataType, ) -> std::result::Result { @@ -846,8 +847,7 @@ fn build_timestamp_from_epoch_fraction( if check_overflow { check_ns_overflow(ns)?; } - let val = ns_to_unit(ns, unit); - builder.append_value(val); + builder.append_value(ns as i64); } } let ns_arr = builder.finish(); @@ -860,7 +860,7 @@ fn build_timestamp_tz_2field( tzoffset: &arrow_array::Int32Array, struct_arr: &arrow_array::StructArray, scale: i64, - unit: TimeUnit, + _unit: TimeUnit, check_overflow: bool, target: DataType, ) -> std::result::Result { @@ -889,7 +889,7 @@ fn build_timestamp_tz_2field( if check_overflow { check_ns_overflow(utc_ns)?; } - builder.append_value(ns_to_unit(utc_ns, unit)); + builder.append_value(utc_ns as i64); } } let ns_arr = builder.finish(); @@ -904,7 +904,7 @@ fn build_timestamp_tz_3field( tzoffset: &arrow_array::Int32Array, struct_arr: &arrow_array::StructArray, scale: i64, - unit: TimeUnit, + _unit: TimeUnit, check_overflow: bool, target: DataType, ) -> std::result::Result { @@ -938,7 +938,7 @@ fn build_timestamp_tz_3field( if check_overflow { check_ns_overflow(utc_ns)?; } - builder.append_value(ns_to_unit(utc_ns, unit)); + builder.append_value(utc_ns as i64); } } let ns_arr = builder.finish(); From 6cb3eeca35b40ca85ae7325510c652bdc6ef8b19 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Fri, 27 Mar 2026 13:25:03 -0400 Subject: [PATCH 42/76] fix(statement): avoid i64 overflow in timestamp builders by converting to target unit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All three timestamp builder functions (build_timestamp_from_epoch_fraction, build_timestamp_tz_2field, build_timestamp_tz_3field) previously stored raw nanoseconds as i64, overflowing for extreme timestamps like 9999-12-31 (253402300799000000000 ns > i64::MAX). Fix: compute ns_to_unit(ns, unit) to convert to the target unit before storing, then reinterpret the Int64 buffer as Timestamp(unit, tz) via build_unchecked — safe because both types share the same i64 physical layout. Also apply roborev findings: fix scale_to_time_unit ceiling semantics, build_timestamp_tz_2field scale bucketing, FIXED branch Int64 cast, bound_batches consistency on error, Date64 div_euclid, NaN guard in get_objects, ts_scale_to_unit/min_time_unit helpers in connection.rs, memory leak on FFI stream error, and trailing whitespace in snowflake.py. --- rust/src/connection.rs | 54 ++++++++--- rust/src/get_objects.rs | 14 ++- rust/src/ingest.rs | 2 +- rust/src/statement.rs | 139 +++++++++++++++++------------ rust/validation/tests/snowflake.py | 2 +- 5 files changed, 135 insertions(+), 76 deletions(-) diff --git a/rust/src/connection.rs b/rust/src/connection.rs index 43747d4..78a215a 100644 --- a/rust/src/connection.rs +++ b/rust/src/connection.rs @@ -619,6 +619,26 @@ impl adbc_core::Connection for Connection { } } +fn ts_scale_to_unit(scale: u32) -> arrow_schema::TimeUnit { + match scale { + 0 => arrow_schema::TimeUnit::Second, + 1..=3 => arrow_schema::TimeUnit::Millisecond, + 4..=6 => arrow_schema::TimeUnit::Microsecond, + _ => arrow_schema::TimeUnit::Nanosecond, + } +} + +fn min_time_unit(a: arrow_schema::TimeUnit, b: arrow_schema::TimeUnit) -> arrow_schema::TimeUnit { + use arrow_schema::TimeUnit::*; + let rank = |u| match u { + Second => 0u8, + Millisecond => 1, + Microsecond => 2, + Nanosecond => 3, + }; + if rank(a) <= rank(b) { a } else { b } +} + fn snowflake_type_to_arrow( type_str: &str, high_precision: bool, @@ -667,13 +687,8 @@ fn snowflake_type_to_arrow( .and_then(|s| type_str.rfind(')').map(|e| &type_str[s + 1..e])) .and_then(|s| s.trim().parse::().ok()) .unwrap_or(9); - let natural = match scale / 3 { - 0 => arrow_schema::TimeUnit::Second, - 1 => arrow_schema::TimeUnit::Millisecond, - 2 => arrow_schema::TimeUnit::Microsecond, - _ => arrow_schema::TimeUnit::Nanosecond, - }; - let unit = if (natural as u32) <= (ts_unit as u32) { natural } else { ts_unit }; + let natural = ts_scale_to_unit(scale); + let unit = min_time_unit(natural, ts_unit); DataType::Timestamp(unit, None) } "TIMESTAMP_LTZ" | "TIMESTAMP_TZ" => { @@ -682,13 +697,8 @@ fn snowflake_type_to_arrow( .and_then(|s| type_str.rfind(')').map(|e| &type_str[s + 1..e])) .and_then(|s| s.trim().parse::().ok()) .unwrap_or(9); - let natural = match scale / 3 { - 0 => arrow_schema::TimeUnit::Second, - 1 => arrow_schema::TimeUnit::Millisecond, - 2 => arrow_schema::TimeUnit::Microsecond, - _ => arrow_schema::TimeUnit::Nanosecond, - }; - let unit = if (natural as u32) <= (ts_unit as u32) { natural } else { ts_unit }; + let natural = ts_scale_to_unit(scale); + let unit = min_time_unit(natural, ts_unit); DataType::Timestamp(unit, Some("UTC".into())) } _ => DataType::Utf8, @@ -841,4 +851,20 @@ mod tests { .collect(); assert_eq!(types, vec!["TABLE", "VIEW"]); } + + #[test] + fn snowflake_type_timestamp_tz_scale6_with_ns_unit_returns_us() { + assert_eq!( + snowflake_type_to_arrow("TIMESTAMP_TZ(6)", true, arrow_schema::TimeUnit::Nanosecond), + DataType::Timestamp(arrow_schema::TimeUnit::Microsecond, Some("UTC".into())) + ); + } + + #[test] + fn snowflake_type_timestamp_ntz_no_parens_defaults_to_ns() { + assert_eq!( + snowflake_type_to_arrow("TIMESTAMP_NTZ", true, arrow_schema::TimeUnit::Nanosecond), + DataType::Timestamp(arrow_schema::TimeUnit::Nanosecond, None) + ); + } } diff --git a/rust/src/get_objects.rs b/rust/src/get_objects.rs index b4cdc20..3cefe5f 100644 --- a/rust/src/get_objects.rs +++ b/rust/src/get_objects.rs @@ -143,10 +143,20 @@ fn cell_to_string(arr: &dyn Array, i: usize) -> Option { // Some NUMBER(p,0) columns (like ordinal_position) arrive as Float64 in the // sf_core Arrow stream for metadata queries; truncate to integer string. if let Some(n) = arr.as_any().downcast_ref::() { - return Some((n.value(i) as i64).to_string()); + let v = n.value(i); + return if v.is_finite() { + Some((v as i64).to_string()) + } else { + None + }; } if let Some(n) = arr.as_any().downcast_ref::() { - return Some((n.value(i) as i64).to_string()); + let v = n.value(i); + return if v.is_finite() { + Some((v as i64).to_string()) + } else { + None + }; } // Snowflake NUMBER(p,0) columns (like ordinal_position) arrive as Decimal128 // when sf_core applies high-precision type mapping. Extract the integer part diff --git a/rust/src/ingest.rs b/rust/src/ingest.rs index c11db2e..a51455e 100644 --- a/rust/src/ingest.rs +++ b/rust/src/ingest.rs @@ -357,7 +357,7 @@ fn value_to_sql(arr: &dyn Array, row: usize, dt: &DataType) -> Result { } if let Some(a) = arr.as_any().downcast_ref::() { // Date64 stores milliseconds since epoch; divide to get days. - let days = a.value(row) / 86_400_000; + let days = a.value(row).div_euclid(86_400_000); return Ok(format!("'{}'::DATE", days_to_date(days))); } diff --git a/rust/src/statement.rs b/rust/src/statement.rs index 5c581f1..97cdb18 100644 --- a/rust/src/statement.rs +++ b/rust/src/statement.rs @@ -24,7 +24,6 @@ use std::sync::Arc; use adbc_core::{ - Optionable, PartitionedResult, error::{Error, Result, Status}, options::{OptionStatement, OptionValue}, @@ -208,7 +207,10 @@ impl Statement { as *mut arrow_array::ffi_stream::FFI_ArrowArrayStream; let reader = unsafe { arrow_array::ffi_stream::ArrowArrayStreamReader::from_raw(raw) } - .map_err(|e| Error::with_message_and_status(e.to_string(), Status::IO))?; + .map_err(|e| { + drop(unsafe { Box::from_raw(raw) }); + Error::with_message_and_status(e.to_string(), Status::IO) + })?; if result_schema.is_none() { result_schema = Some(reader.schema()); @@ -279,8 +281,9 @@ impl adbc_core::Statement for Statement { fn execute(&mut self) -> Result> { if self.target_table.is_some() { // Ingest via execute() — run the ingest and return an empty reader. - crate::ingest::execute_ingest(self)?; + let result = crate::ingest::execute_ingest(self); self.bound_batches.clear(); + result?; let batch = arrow_array::RecordBatch::new_empty(Arc::new(arrow_schema::Schema::empty())); return Ok(Box::new(crate::connection::SingleBatchReader::new(batch))); @@ -316,7 +319,10 @@ impl adbc_core::Statement for Statement { let raw = Box::into_raw(result.stream) as *mut arrow_array::ffi_stream::FFI_ArrowArrayStream; let reader = unsafe { arrow_array::ffi_stream::ArrowArrayStreamReader::from_raw(raw) } - .map_err(|e| Error::with_message_and_status(e.to_string(), Status::IO))?; + .map_err(|e| { + drop(unsafe { Box::from_raw(raw) }); + Error::with_message_and_status(e.to_string(), Status::IO) + })?; Ok(Box::new(ConvertingReader::new(reader, self.use_high_precision, self.timestamp_precision.time_unit()))) } @@ -485,10 +491,10 @@ impl RecordBatchReader for ConcatReader { // ── Schema adjustment and type conversions ──────────────────────────────── fn scale_to_time_unit(scale: u32) -> TimeUnit { - match scale / 3 { + match scale { 0 => TimeUnit::Second, - 1 => TimeUnit::Millisecond, - 2 => TimeUnit::Microsecond, + 1..=3 => TimeUnit::Millisecond, + 4..=6 => TimeUnit::Microsecond, _ => TimeUnit::Nanosecond, } } @@ -658,7 +664,7 @@ impl ConvertingReader { _ => arrow_cast::cast(col.as_ref(), &DataType::Int64), } } else if use_high_precision { - Ok(col.clone()) + arrow_cast::cast(col.as_ref(), &DataType::Int64) } else { // Cast to Float64, then divide by 10^scale to restore decimal value let casted = arrow_cast::cast(col.as_ref(), &DataType::Float64)?; @@ -709,7 +715,7 @@ fn convert_timestamp_ntz( match col.data_type() { DataType::Int64 => { - let natural_unit = scale_to_time_unit(scale as u32); + let natural_unit = scale_to_time_unit(scale.max(0) as u32); if natural_unit == unit { arrow_cast::cast(col.as_ref(), &target) } else { @@ -739,7 +745,7 @@ fn convert_timestamp_ntz( arrow_schema::ArrowError::CastError("expected Int32 fraction".into()) })?; - build_timestamp_from_epoch_fraction(epoch, fraction, struct_arr, scale, unit, check_overflow, target) + build_timestamp_from_epoch_fraction(epoch, fraction, struct_arr, scale, check_overflow, target) } _ => arrow_cast::cast(col.as_ref(), &target), } @@ -775,7 +781,7 @@ fn convert_timestamp_tz( .downcast_ref::() .ok_or_else(|| arrow_schema::ArrowError::CastError("expected Int32 timezone".into()))?; - build_timestamp_tz_2field(epoch, tzoffset, struct_arr, scale, unit, check_overflow, target) + build_timestamp_tz_2field(epoch, tzoffset, struct_arr, scale, check_overflow, target) } else { let fraction = struct_arr .column(1) @@ -788,21 +794,10 @@ fn convert_timestamp_tz( .downcast_ref::() .ok_or_else(|| arrow_schema::ArrowError::CastError("expected Int32 timezone".into()))?; - build_timestamp_tz_3field(epoch, fraction, tzoffset, struct_arr, scale, unit, check_overflow, target) + build_timestamp_tz_3field(epoch, fraction, tzoffset, struct_arr, scale, check_overflow, target) } } -#[allow(dead_code)] -fn ns_to_unit(ns: i128, unit: TimeUnit) -> i64 { - let divisor: i128 = match unit { - TimeUnit::Second => 1_000_000_000, - TimeUnit::Millisecond => 1_000_000, - TimeUnit::Microsecond => 1_000, - TimeUnit::Nanosecond => 1, - }; - (ns / divisor) as i64 -} - fn check_ns_overflow(ns: i128) -> std::result::Result<(), arrow_schema::ArrowError> { if ns > i64::MAX as i128 || ns < i64::MIN as i128 { Err(arrow_schema::ArrowError::CastError(format!( @@ -813,17 +808,28 @@ fn check_ns_overflow(ns: i128) -> std::result::Result<(), arrow_schema::ArrowErr } } +fn ns_to_unit(ns: i128, unit: TimeUnit) -> i64 { + let val = match unit { + TimeUnit::Second => ns / 1_000_000_000, + TimeUnit::Millisecond => ns / 1_000_000, + TimeUnit::Microsecond => ns / 1_000, + TimeUnit::Nanosecond => ns, + }; + val as i64 +} + fn build_timestamp_from_epoch_fraction( epoch: &arrow_array::Int64Array, fraction: &arrow_array::Int32Array, struct_arr: &arrow_array::StructArray, scale: i64, - _unit: TimeUnit, check_overflow: bool, target: DataType, ) -> std::result::Result { - use arrow_array::builder::PrimitiveBuilder; - use arrow_array::types::TimestampNanosecondType; + let unit = match &target { + DataType::Timestamp(u, _) => *u, + _ => unreachable!("target must be Timestamp"), + }; let len = epoch.len(); let frac_to_ns: i128 = if scale <= 9 { @@ -832,10 +838,10 @@ fn build_timestamp_from_epoch_fraction( 1 }; - let mut builder = PrimitiveBuilder::::with_capacity(len); + let mut values: Vec> = Vec::with_capacity(len); for i in 0..len { if struct_arr.is_null(i) { - builder.append_null(); + values.push(None); } else { let ns: i128 = if epoch.value(i) >= 0 { epoch.value(i) as i128 * 1_000_000_000 @@ -847,12 +853,18 @@ fn build_timestamp_from_epoch_fraction( if check_overflow { check_ns_overflow(ns)?; } - builder.append_value(ns as i64); + values.push(Some(ns_to_unit(ns, unit))); } } - let ns_arr = builder.finish(); - let intermediate: Arc = Arc::new(ns_arr); - arrow_cast::cast(intermediate.as_ref(), &target) + let int_arr = arrow_array::Int64Array::from(values); + let data = unsafe { + int_arr + .into_data() + .into_builder() + .data_type(target.clone()) + .build_unchecked() + }; + Ok(Arc::new(arrow_array::make_array(data)) as ArrayRef) } fn build_timestamp_tz_2field( @@ -860,41 +872,42 @@ fn build_timestamp_tz_2field( tzoffset: &arrow_array::Int32Array, struct_arr: &arrow_array::StructArray, scale: i64, - _unit: TimeUnit, check_overflow: bool, target: DataType, ) -> std::result::Result { - use arrow_array::builder::PrimitiveBuilder; - use arrow_array::types::TimestampNanosecondType; + let unit = match &target { + DataType::Timestamp(u, _) => *u, + _ => unreachable!("target must be Timestamp"), + }; let len = epoch.len(); - let mut builder = PrimitiveBuilder::::with_capacity(len); + let mut values: Vec> = Vec::with_capacity(len); for i in 0..len { if struct_arr.is_null(i) { - builder.append_null(); + values.push(None); } else { let tz_offset_minutes: i128 = (tzoffset.value(i) as i128) - 1440; let tz_offset_ns: i128 = tz_offset_minutes * 60 * 1_000_000_000; - let epoch_ns: i128 = match scale { - 0..=2 => epoch.value(i) as i128 * 1_000_000_000, - 3..=5 => epoch.value(i) as i128 * 1_000_000, - 6..=8 => epoch.value(i) as i128 * 1_000, - 9 => epoch.value(i) as i128, - _ => epoch.value(i) as i128 * 1_000_000_000, - }; + let epoch_ns: i128 = epoch.value(i) as i128 * 10i128.pow((9u32).saturating_sub(scale.clamp(0, 9) as u32)); let utc_ns = epoch_ns - tz_offset_ns; if check_overflow { check_ns_overflow(utc_ns)?; } - builder.append_value(utc_ns as i64); + values.push(Some(ns_to_unit(utc_ns, unit))); } } - let ns_arr = builder.finish(); - let intermediate: Arc = Arc::new(ns_arr); - arrow_cast::cast(intermediate.as_ref(), &target) + let int_arr = arrow_array::Int64Array::from(values); + let data = unsafe { + int_arr + .into_data() + .into_builder() + .data_type(target.clone()) + .build_unchecked() + }; + Ok(Arc::new(arrow_array::make_array(data)) as ArrayRef) } #[allow(clippy::too_many_arguments)] @@ -904,12 +917,13 @@ fn build_timestamp_tz_3field( tzoffset: &arrow_array::Int32Array, struct_arr: &arrow_array::StructArray, scale: i64, - _unit: TimeUnit, check_overflow: bool, target: DataType, ) -> std::result::Result { - use arrow_array::builder::PrimitiveBuilder; - use arrow_array::types::TimestampNanosecondType; + let unit = match &target { + DataType::Timestamp(u, _) => *u, + _ => unreachable!("target must be Timestamp"), + }; let len = epoch.len(); let frac_to_ns: i128 = if scale <= 9 { @@ -917,11 +931,11 @@ fn build_timestamp_tz_3field( } else { 1 }; - let mut builder = PrimitiveBuilder::::with_capacity(len); + let mut values: Vec> = Vec::with_capacity(len); for i in 0..len { if struct_arr.is_null(i) { - builder.append_null(); + values.push(None); } else { let tz_offset_minutes: i128 = (tzoffset.value(i) as i128) - 1440; let tz_offset_ns: i128 = tz_offset_minutes * 60 * 1_000_000_000; @@ -938,12 +952,18 @@ fn build_timestamp_tz_3field( if check_overflow { check_ns_overflow(utc_ns)?; } - builder.append_value(utc_ns as i64); + values.push(Some(ns_to_unit(utc_ns, unit))); } } - let ns_arr = builder.finish(); - let intermediate: Arc = Arc::new(ns_arr); - arrow_cast::cast(intermediate.as_ref(), &target) + let int_arr = arrow_array::Int64Array::from(values); + let data = unsafe { + int_arr + .into_data() + .into_builder() + .data_type(target.clone()) + .build_unchecked() + }; + Ok(Arc::new(arrow_array::make_array(data)) as ArrayRef) } impl Iterator for ConvertingReader { @@ -955,6 +975,9 @@ impl Iterator for ConvertingReader { Err(e) => return Some(Err(e)), }; + // check_overflow is intentionally false: overflow truncation is accepted + // for far-future/far-past timestamps at nanosecond precision. Callers + // that need strict overflow detection should use a separate validation step. let check_overflow = false; let adjusted_columns: std::result::Result, arrow_schema::ArrowError> = batch @@ -1606,4 +1629,4 @@ mod tests { assert_eq!(result.field(0).data_type(), &DataType::Timestamp(TimeUnit::Microsecond, None)); } -} \ No newline at end of file +} diff --git a/rust/validation/tests/snowflake.py b/rust/validation/tests/snowflake.py index 2dcfa81..2702c9b 100644 --- a/rust/validation/tests/snowflake.py +++ b/rust/validation/tests/snowflake.py @@ -57,7 +57,7 @@ class SnowflakeQuirks(model.DriverQuirks): "uri": model.FromEnv("SNOWFLAKE_URI"), "adbc.snowflake.sql.db": model.FromEnv("SNOWFLAKE_DATABASE"), "adbc.snowflake.sql.schema": model.FromEnv("SNOWFLAKE_SCHEMA"), - "adbc.snowflake.sql.client_option.use_high_precision": "false", + "adbc.snowflake.sql.client_option.use_high_precision": "false", "timezone": "UTC", }, connection={}, From 691e3b7dc1c925e5dbd0218fc5300e907cb8733f Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Fri, 27 Mar 2026 13:34:52 -0400 Subject: [PATCH 43/76] fix(statement): clamp scale in fraction builders; document ns_to_unit truncation and FFI safety Clamp scale to [0,9] in build_timestamp_from_epoch_fraction and build_timestamp_tz_3field before computing frac_to_ns to prevent negative-scale panic or wrap-around on non-Snowflake callers. Add inline comments documenting: intentional i64 truncation in the Nanosecond arm of ns_to_unit, and Arrow C Data Interface contract (from_raw does not call release on failure) in both FFI map_err closures. --- rust/src/statement.rs | 44 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/rust/src/statement.rs b/rust/src/statement.rs index 97cdb18..54e5f7a 100644 --- a/rust/src/statement.rs +++ b/rust/src/statement.rs @@ -208,6 +208,9 @@ impl Statement { let reader = unsafe { arrow_array::ffi_stream::ArrowArrayStreamReader::from_raw(raw) } .map_err(|e| { + // Safety: Arrow's C Data Interface specifies that on failure, from_raw + // does NOT call the stream's release callback, so reconstructing the + // Box here is the only release path — no double-free risk. drop(unsafe { Box::from_raw(raw) }); Error::with_message_and_status(e.to_string(), Status::IO) })?; @@ -320,6 +323,9 @@ impl adbc_core::Statement for Statement { Box::into_raw(result.stream) as *mut arrow_array::ffi_stream::FFI_ArrowArrayStream; let reader = unsafe { arrow_array::ffi_stream::ArrowArrayStreamReader::from_raw(raw) } .map_err(|e| { + // Safety: Arrow's C Data Interface specifies that on failure, from_raw + // does NOT call the stream's release callback, so reconstructing the + // Box here is the only release path — no double-free risk. drop(unsafe { Box::from_raw(raw) }); Error::with_message_and_status(e.to_string(), Status::IO) })?; @@ -813,6 +819,8 @@ fn ns_to_unit(ns: i128, unit: TimeUnit) -> i64 { TimeUnit::Second => ns / 1_000_000_000, TimeUnit::Millisecond => ns / 1_000_000, TimeUnit::Microsecond => ns / 1_000, + // Intentional truncation: ns timestamps outside i64 range silently wrap. + // Callers that need strict overflow detection should pass check_overflow=true. TimeUnit::Nanosecond => ns, }; val as i64 @@ -832,6 +840,7 @@ fn build_timestamp_from_epoch_fraction( }; let len = epoch.len(); + let scale = scale.clamp(0, 9); let frac_to_ns: i128 = if scale <= 9 { 10i128.pow((9 - scale) as u32) } else { @@ -926,6 +935,7 @@ fn build_timestamp_tz_3field( }; let len = epoch.len(); + let scale = scale.clamp(0, 9); let frac_to_ns: i128 = if scale <= 9 { 10i128.pow((9 - scale) as u32) } else { @@ -1629,4 +1639,38 @@ mod tests { assert_eq!(result.field(0).data_type(), &DataType::Timestamp(TimeUnit::Microsecond, None)); } + #[test] + fn test_build_timestamp_tz_2field_year9999() { + use arrow_array::{Int64Array, Int32Array, StructArray}; + use arrow_schema::Field as SchemaField; + + let epoch_us: i64 = 253402300799000000; // 9999-12-31T23:59:59Z in microseconds + let epoch_arr = Int64Array::from(vec![epoch_us]); + let tz_arr = Int32Array::from(vec![1440i32]); // UTC + + let fields = vec![ + Arc::new(SchemaField::new("epoch", DataType::Int64, false)), + Arc::new(SchemaField::new("tz", DataType::Int32, false)), + ]; + let struct_arr = StructArray::try_new( + fields.into(), + vec![Arc::new(epoch_arr) as ArrayRef, Arc::new(tz_arr) as ArrayRef], + None, + ).unwrap(); + + let epoch_col = struct_arr.column(0).as_any().downcast_ref::().unwrap(); + let tz_col = struct_arr.column(1).as_any().downcast_ref::().unwrap(); + + let result = build_timestamp_tz_2field( + epoch_col, tz_col, &struct_arr, 6, false, + DataType::Timestamp(TimeUnit::Microsecond, Some(Arc::from("UTC"))), + ).unwrap(); + + let ts = result + .as_any() + .downcast_ref::() + .unwrap(); + assert_eq!(ts.value(0), epoch_us, "year 9999 should round-trip as microseconds"); + } + } From fb1f3abaaabe9e7cc570e19551f98bfcacc208c7 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Fri, 27 Mar 2026 17:53:47 -0400 Subject: [PATCH 44/76] fix: remove client_app_id override to get Arrow format; fix negative epoch fraction formula MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the custom client_app_id ('[ADBC][Rust] Snowflake Driver/0.1.0') that caused Snowflake to return queryResultFormat='json' instead of 'arrow'. Snowflake only recognizes known client identifiers like 'PythonConnector' for Arrow format; unrecognized clients get JSON, which truncates floats to ~10 significant digits and converts DBL_MAX to infinity. Letting sf_core use its default 'PythonConnector' restores full IEEE 754 float64 precision. Fix negative epoch handling in build_timestamp_from_epoch_fraction and build_timestamp_tz_3field: Snowflake uses floor division for epoch/ fraction decomposition so the fraction is always non-negative — always add (never subtract) the fraction regardless of epoch sign. Format Float32 bind parameters with f32 precision ({v:?}) instead of casting to f64 first, matching the shortest round-trip representation. Update float64 bind expected values to match full-precision output. Add integration test for float64 select precision with bit-exact assertions. --- rust/src/driver.rs | 10 +--- rust/src/ingest.rs | 2 +- rust/src/statement.rs | 20 ++------ rust/tests/integration.rs | 49 ++++++++++++++++++- .../validation/queries/type/bind/float64.json | 2 +- 5 files changed, 57 insertions(+), 26 deletions(-) diff --git a/rust/src/driver.rs b/rust/src/driver.rs index a26a016..545cdf3 100644 --- a/rust/src/driver.rs +++ b/rust/src/driver.rs @@ -24,9 +24,9 @@ use std::sync::Arc; use adbc_core::{ - Optionable, error::{Error, Result, Status}, options::{OptionDatabase, OptionValue}, + Optionable, }; use arrow_schema::TimeUnit; use sf_core::apis::database_driver_v1::DatabaseDriverV1; @@ -100,14 +100,8 @@ impl adbc_core::Driver for Driver { opts: impl IntoIterator, ) -> Result { let db_handle = self.inner.sf.database_new(); - let mut sf_settings: std::collections::HashMap = + let sf_settings: std::collections::HashMap = Default::default(); - sf_settings.insert( - "client_app_id".to_string(), - sf_core::config::settings::Setting::String( - concat!("[ADBC][Rust] Snowflake Driver/", env!("CARGO_PKG_VERSION")).to_string(), - ), - ); let mut db = Database { inner: self.inner.clone(), db_handle, diff --git a/rust/src/ingest.rs b/rust/src/ingest.rs index a51455e..af61602 100644 --- a/rust/src/ingest.rs +++ b/rust/src/ingest.rs @@ -316,7 +316,7 @@ fn value_to_sql(arr: &dyn Array, row: usize, dt: &DataType) -> Result { if let Some(a) = arr.as_any().downcast_ref::() { let v = a.value(row); return if v.is_finite() { - Ok(format!("{:?}", v as f64)) + Ok(format!("{v:?}")) } else { Ok("NULL".to_string()) }; diff --git a/rust/src/statement.rs b/rust/src/statement.rs index 54e5f7a..aa5eb1e 100644 --- a/rust/src/statement.rs +++ b/rust/src/statement.rs @@ -852,13 +852,8 @@ fn build_timestamp_from_epoch_fraction( if struct_arr.is_null(i) { values.push(None); } else { - let ns: i128 = if epoch.value(i) >= 0 { - epoch.value(i) as i128 * 1_000_000_000 - + fraction.value(i) as i128 * frac_to_ns - } else { - epoch.value(i) as i128 * 1_000_000_000 - - fraction.value(i) as i128 * frac_to_ns - }; + let ns: i128 = epoch.value(i) as i128 * 1_000_000_000 + + fraction.value(i) as i128 * frac_to_ns; if check_overflow { check_ns_overflow(ns)?; } @@ -950,13 +945,8 @@ fn build_timestamp_tz_3field( let tz_offset_minutes: i128 = (tzoffset.value(i) as i128) - 1440; let tz_offset_ns: i128 = tz_offset_minutes * 60 * 1_000_000_000; - let epoch_ns: i128 = if epoch.value(i) >= 0 { - epoch.value(i) as i128 * 1_000_000_000 - + fraction.value(i) as i128 * frac_to_ns - } else { - epoch.value(i) as i128 * 1_000_000_000 - - fraction.value(i) as i128 * frac_to_ns - }; + let epoch_ns: i128 = epoch.value(i) as i128 * 1_000_000_000 + + fraction.value(i) as i128 * frac_to_ns; let utc_ns = epoch_ns - tz_offset_ns; if check_overflow { @@ -1135,7 +1125,7 @@ fn arrow_value_to_sql_literal(arr: &dyn Array, row: usize) -> Result { if let Some(a) = arr.as_any().downcast_ref::() { let v = a.value(row); return if v.is_finite() { - Ok(format!("{:?}", v as f64)) + Ok(format!("{v:?}")) } else { Ok("NULL".to_string()) }; diff --git a/rust/tests/integration.rs b/rust/tests/integration.rs index 49a198d..aab6643 100644 --- a/rust/tests/integration.rs +++ b/rust/tests/integration.rs @@ -22,11 +22,12 @@ // tests/integration.rs use adbc_core::{ - Connection as _, Database as _, Driver as _, Optionable, Statement as _, options::{OptionConnection, OptionDatabase, OptionValue}, + Connection as _, Database as _, Driver as _, Optionable, Statement as _, }; use adbc_driver_snowflake::{Database, Driver}; use arrow_array::cast::AsArray; +use arrow_array::Array; use arrow_schema::{DataType, TimeUnit}; fn get_env(key: &str) -> Option { @@ -644,3 +645,49 @@ fn test_execute_schema() { ); } } + +#[test] +fn test_float64_select_precision() { + let Some(mut conn) = make_connection() else { + return; + }; + let mut stmt = conn.new_statement().expect("new_statement"); + + stmt.set_sql_query( + "SELECT 3.14159265358979::FLOAT as pi, \ + 0.0::FLOAT as zero, \ + -1.7976931348623157e308::FLOAT as neg_max, \ + 1.7976931348623157e308::FLOAT as pos_max, \ + 2.2250738585072014e-308::FLOAT as min_pos, \ + NULL::FLOAT as null_val", + ) + .expect("set_sql_query"); + + let mut reader = stmt.execute().expect("execute"); + let batch = reader.next().expect("batch").expect("ok"); + + let check = |col_idx: usize, name: &str, expected_bits: u64| { + let arr = batch + .column(col_idx) + .as_primitive::(); + let v = arr.value(0); + assert_eq!( + v.to_bits(), + expected_bits, + "{name}: got {v} (bits={:#018x}), expected bits={:#018x}", + v.to_bits(), + expected_bits + ); + }; + + check(0, "pi", 0x400921fb54442d11); + check(1, "zero", 0x0000000000000000); + check(2, "neg_max", 0xffefffffffffffff); + check(3, "pos_max", 0x7fefffffffffffff); + check(4, "min_pos", 0x0010000000000000); + + let null_arr = batch + .column(5) + .as_primitive::(); + assert!(null_arr.is_null(0), "null_val should be null"); +} diff --git a/rust/validation/queries/type/bind/float64.json b/rust/validation/queries/type/bind/float64.json index 15ffcd5..7d0f7f1 100644 --- a/rust/validation/queries/type/bind/float64.json +++ b/rust/validation/queries/type/bind/float64.json @@ -2,5 +2,5 @@ {"res": -1.797693e+38} {"res": -2.5} {"res": 0.0} -{"res": 3.1415927} +{"res": 3.141592653589793} {"res": 1.797693e+38} From 38fe7e4a02ceb9a191dd20ba8e6487b8465bbb32 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Fri, 27 Mar 2026 18:27:33 -0400 Subject: [PATCH 45/76] feat(statement): implement Decimal128 high-precision support for FIXED columns When use_high_precision=true and scale>0, FIXED columns are now returned as Decimal128(precision, scale) instead of Int64. Matches Go driver behavior: Int64 source is cast to Decimal128(20,0) then reinterpreted with the correct precision/scale metadata. Decimal128 source from Arrow-format responses passes through as identity. When use_high_precision=false, Decimal128 source columns are properly cast to Float64 without double-dividing by the scale factor. Fix test_execute_schema assertion: bare TIMESTAMP_NTZ defaults to scale 9 (nanoseconds), not microseconds. --- rust/src/statement.rs | 159 +++++++++++++++++++++++++++++++++----- rust/tests/integration.rs | 2 +- 2 files changed, 141 insertions(+), 20 deletions(-) diff --git a/rust/src/statement.rs b/rust/src/statement.rs index aa5eb1e..cd177df 100644 --- a/rust/src/statement.rs +++ b/rust/src/statement.rs @@ -555,10 +555,17 @@ fn compute_target_type(field: &Field, use_high_precision: bool, ts_unit: TimeUni .get("scale") .and_then(|s| s.parse().ok()) .unwrap_or(0); + let precision: u8 = field + .metadata() + .get("precision") + .and_then(|s| s.parse().ok()) + .unwrap_or(38); match logical_type { "FIXED" => { - if scale == 0 || use_high_precision { + if use_high_precision && scale > 0 { + DataType::Decimal128(precision, scale as i8) + } else if scale == 0 { DataType::Int64 } else { DataType::Float64 @@ -670,22 +677,47 @@ impl ConvertingReader { _ => arrow_cast::cast(col.as_ref(), &DataType::Int64), } } else if use_high_precision { - arrow_cast::cast(col.as_ref(), &DataType::Int64) + match col.data_type() { + // Source is already Decimal128 — pass through as identity + DataType::Decimal128(_, _) => Ok(col.clone()), + // Source is Int64 (scaled integer) — convert to Decimal128(precision, scale) + // following Go's integerToDecimal128: cast Int64 → Decimal128(20,0), + // then reinterpret as Decimal128(precision, scale). + _ => { + let intermediate = arrow_cast::cast( + col.as_ref(), + &DataType::Decimal128(20, 0), + )?; + let data = intermediate.to_data(); + let retyped = unsafe { + data.into_builder() + .data_type(target_type.clone()) + .build_unchecked() + }; + Ok(arrow_array::make_array(retyped)) + } + } } else { - // Cast to Float64, then divide by 10^scale to restore decimal value - let casted = arrow_cast::cast(col.as_ref(), &DataType::Float64)?; - let divisor = 10f64.powi(scale as i32); - let float_arr = casted - .as_any() - .downcast_ref::() - .ok_or_else(|| { - arrow_schema::ArrowError::CastError( - "expected Float64Array after cast".into(), - ) - })?; - let divided: arrow_array::Float64Array = - float_arr.iter().map(|v| v.map(|x| x / divisor)).collect(); - Ok(Arc::new(divided) as ArrayRef) + match col.data_type() { + DataType::Decimal128(_, _) => { + arrow_cast::cast(col.as_ref(), &DataType::Float64) + } + _ => { + let casted = arrow_cast::cast(col.as_ref(), &DataType::Float64)?; + let divisor = 10f64.powi(scale as i32); + let float_arr = casted + .as_any() + .downcast_ref::() + .ok_or_else(|| { + arrow_schema::ArrowError::CastError( + "expected Float64Array after cast".into(), + ) + })?; + let divided: arrow_array::Float64Array = + float_arr.iter().map(|v| v.map(|x| x / divisor)).collect(); + Ok(Arc::new(divided) as ArrayRef) + } + } } } "TIME" => arrow_cast::cast(col.as_ref(), target_type), @@ -1521,9 +1553,20 @@ mod tests { assert_eq!(col2.values(), &[100i64, 200]); } fn make_field_with_meta(name: &str, dt: DataType, logical_type: &str, scale: &str) -> Field { + make_field_with_precision(name, dt, logical_type, scale, "38") + } + + fn make_field_with_precision( + name: &str, + dt: DataType, + logical_type: &str, + scale: &str, + precision: &str, + ) -> Field { let mut md = std::collections::HashMap::new(); md.insert("logicalType".to_string(), logical_type.to_string()); md.insert("scale".to_string(), scale.to_string()); + md.insert("precision".to_string(), precision.to_string()); Field::new(name, dt, true).with_metadata(md) } @@ -1552,11 +1595,11 @@ mod tests { } #[test] - fn test_adjust_schema_fixed_scale2_high_precision_stays_int64() { - let f = make_field_with_meta("x", DataType::Int64, "FIXED", "2"); + fn test_adjust_schema_fixed_scale2_high_precision_is_decimal128() { + let f = make_field_with_precision("x", DataType::Int64, "FIXED", "2", "10"); let schema = Schema::new(vec![f]); let result = adjust_schema(&schema, true, TimeUnit::Nanosecond); - assert_eq!(result.field(0).data_type(), &DataType::Int64); + assert_eq!(result.field(0).data_type(), &DataType::Decimal128(10, 2)); } #[test] @@ -1596,6 +1639,84 @@ mod tests { assert!((col.value(1) - 2.55).abs() < 1e-9); } + #[test] + fn test_converting_reader_fixed_scale2_high_precision_produces_decimal128() { + let f = make_field_with_precision("x", DataType::Int64, "FIXED", "2", "10"); + let schema = Arc::new(Schema::new(vec![f])); + let batch = RecordBatch::try_new( + schema.clone(), + vec![Arc::new(arrow_array::Int64Array::from(vec![12345i64, -255]))], + ) + .unwrap(); + let reader = ConcatReader { batches: vec![batch].into_iter(), schema }; + let mut cr = ConvertingReader::new(reader, true, TimeUnit::Nanosecond); + let out = cr.next().unwrap().unwrap(); + assert_eq!(out.schema().field(0).data_type(), &DataType::Decimal128(10, 2)); + let col = out.column(0).as_any().downcast_ref::().unwrap(); + // 12345 with scale 2 = 123.45 + assert_eq!(col.value(0), 12345i128); + // -255 with scale 2 = -2.55 + assert_eq!(col.value(1), -255i128); + } + + #[test] + fn test_converting_reader_fixed_scale0_high_precision_stays_int64() { + let f = make_field_with_precision("x", DataType::Int64, "FIXED", "0", "10"); + let schema = Arc::new(Schema::new(vec![f])); + let batch = RecordBatch::try_new( + schema.clone(), + vec![Arc::new(arrow_array::Int64Array::from(vec![42i64]))], + ) + .unwrap(); + let reader = ConcatReader { batches: vec![batch].into_iter(), schema }; + let mut cr = ConvertingReader::new(reader, true, TimeUnit::Nanosecond); + let out = cr.next().unwrap().unwrap(); + assert_eq!(out.schema().field(0).data_type(), &DataType::Int64); + } + + #[test] + fn test_converting_reader_decimal128_source_high_precision_identity() { + let f = make_field_with_precision("x", DataType::Decimal128(10, 2), "FIXED", "2", "10"); + let schema = Arc::new(Schema::new(vec![f])); + let batch = RecordBatch::try_new( + schema.clone(), + vec![Arc::new( + arrow_array::Decimal128Array::from(vec![12345i128, -255]) + .with_precision_and_scale(10, 2) + .unwrap(), + )], + ) + .unwrap(); + let reader = ConcatReader { batches: vec![batch].into_iter(), schema }; + let mut cr = ConvertingReader::new(reader, true, TimeUnit::Nanosecond); + let out = cr.next().unwrap().unwrap(); + assert_eq!(out.schema().field(0).data_type(), &DataType::Decimal128(10, 2)); + let col = out.column(0).as_any().downcast_ref::().unwrap(); + assert_eq!(col.value(0), 12345i128); + assert_eq!(col.value(1), -255i128); + } + + #[test] + fn test_converting_reader_decimal128_source_no_high_precision_casts_to_float64() { + let f = make_field_with_precision("x", DataType::Decimal128(10, 2), "FIXED", "2", "10"); + let schema = Arc::new(Schema::new(vec![f])); + let batch = RecordBatch::try_new( + schema.clone(), + vec![Arc::new( + arrow_array::Decimal128Array::from(vec![12345i128]) + .with_precision_and_scale(10, 2) + .unwrap(), + )], + ) + .unwrap(); + let reader = ConcatReader { batches: vec![batch].into_iter(), schema }; + let mut cr = ConvertingReader::new(reader, false, TimeUnit::Nanosecond); + let out = cr.next().unwrap().unwrap(); + assert_eq!(out.schema().field(0).data_type(), &DataType::Float64); + let col = out.column(0).as_any().downcast_ref::().unwrap(); + assert!((col.value(0) - 123.45).abs() < 1e-9); + } + #[test] fn test_converting_reader_timestamp_ntz_int64_cast() { let f = make_field_with_meta("ts", DataType::Int64, "TIMESTAMP_NTZ", "9"); diff --git a/rust/tests/integration.rs b/rust/tests/integration.rs index aab6643..2035482 100644 --- a/rust/tests/integration.rs +++ b/rust/tests/integration.rs @@ -641,7 +641,7 @@ fn test_execute_schema() { assert_eq!(schema.field(1).data_type(), &DataType::Utf8); assert_eq!( schema.field(2).data_type(), - &DataType::Timestamp(TimeUnit::Microsecond, None) + &DataType::Timestamp(TimeUnit::Nanosecond, None) ); } } From 905bad2095c68eab346274734dfc025431933532 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Fri, 27 Mar 2026 18:30:23 -0400 Subject: [PATCH 46/76] docs: add README tracking sf_core and Snowflake limitations --- rust/validation/README_sf_core_limitations.md | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 rust/validation/README_sf_core_limitations.md diff --git a/rust/validation/README_sf_core_limitations.md b/rust/validation/README_sf_core_limitations.md new file mode 100644 index 0000000..5e8fb49 --- /dev/null +++ b/rust/validation/README_sf_core_limitations.md @@ -0,0 +1,52 @@ +# Snowflake and sf_core Limitations in the Rust ADBC Driver + +During the implementation and validation of the Rust ADBC Snowflake driver, we encountered several architectural limitations in both the Snowflake backend and the `sf_core` library. The driver currently employs workarounds for these issues. + +This document serves as a reference for future improvements that should ideally be made upstream in `sf_core` or Snowflake itself. + +## 1. Arrow IPC Format and `client_app_id` +**Issue:** Snowflake's backend determines the default `queryResultFormat` (Arrow IPC vs JSON rowset) based on the `CLIENT_APP_ID` field sent during the login request. Unrecognized client IDs fall back to JSON rowset. +**Impact:** JSON rowset format truncates `FLOAT`/`REAL` values to ~10 significant digits (losing IEEE 754 double precision) and converts `DBL_MAX` to `inf`. +**Workaround in Driver:** The driver currently omits overriding `client_app_id` (leaving it as `sf_core`'s default `"PythonConnector"`). This tricks Snowflake into recognizing the client and returning true Arrow IPC payloads with full-precision floats. +**Ideal Fix:** +- **Snowflake backend:** Honor `ALTER SESSION SET QUERY_RESULT_FORMAT = 'ARROW_FORCE'` uniformly, regardless of `CLIENT_APP_ID`. +- **sf_core:** Expose a native way to request Arrow format reliably without depending on a specific `CLIENT_APP_ID`, or ensure that custom client IDs can explicitly opt into Arrow format. + +## 2. Float Bind Parameter Precision +**Issue:** When binding `Float64` parameters, Snowflake relies on string representation if using standard substitution. The Go driver (`gosnowflake`) formats float64 bind parameters with `FormatFloat(..., 32)` (float32 precision, ~7 significant digits), permanently truncating `3.141592653589793` to `"3.1415927"`. +**Impact:** Test expectations written for the Go driver expect truncated bind values. +**Workaround in Driver:** The Rust driver uses `format!("{v:?}")` for `Float64` (preserving full precision). For `Float32`, it avoids casting to `f64` first to prevent precision expansion (e.g. `3.14159` → `3.141590118408203`), preserving exact float32 semantics. +**Ideal Fix:** +- **Go Driver / gosnowflake:** Stop truncating float64 bind parameters to 32-bit precision. +- **sf_core:** Implement native Arrow batch binding (via the Arrow IPC streaming endpoint) rather than relying on SQL string substitution for bind parameters. + +## 3. Timestamp Decoding (Epoch + Fraction) +**Issue:** `sf_core` decodes `TIMESTAMP_NTZ`/`TIMESTAMP_TZ` columns as a struct containing `epoch` (seconds, Int64) and `fraction` (nanoseconds, Int32/Int64). For pre-1970 timestamps (negative epochs), Snowflake uses floor division (e.g., `-9223372037` seconds + `145224192` nanoseconds), meaning the fraction is always positive. +**Impact:** Calculating total nanoseconds requires careful sign handling. If the fraction is subtracted when the epoch is negative (standard C-style truncation logic), the resulting date is off by ~1 second. +**Workaround in Driver:** The `ConvertingReader` must explicitly add the fraction regardless of the epoch's sign: `epoch * 1_000_000_000 + fraction * frac_to_ns`. +**Ideal Fix:** +- **sf_core:** When requested, natively map `TIMESTAMP_*` columns directly to Arrow `Timestamp` primitive arrays in the returned FFI stream, hiding the epoch/fraction struct complexity from the consumer. + +## 4. Scaled Integers (`FIXED` type) +**Issue:** `sf_core` returns `FIXED` (e.g., `NUMBER(10,3)`) columns as raw `Int64` buffers representing the scaled integer (e.g., `12345` for `12.345`). The Arrow FFI stream schema only reports `Int64` with custom metadata (`logicalType: "FIXED"`, `scale: "3"`). +**Impact:** The ADBC driver has to implement a complex `ConvertingReader` to interpret this metadata and manually cast `Int64` to `Float64` (dividing by `10^scale`) or `Decimal128`. +**Workaround in Driver:** We intercept the Arrow FFI stream, read the metadata, and perform a secondary pass using `arrow_cast::cast` + manual division (for `Float64`) or unsafe metadata rewriting (`build_unchecked`) for `Decimal128`. +**Ideal Fix:** +- **sf_core:** Automatically apply the scale. If `sf_core` detects a `FIXED` type with `scale > 0`, it should export the FFI stream as `Decimal128` natively. Arrow already supports `Decimal128Type`, which perfectly represents Snowflake's scaled integers without needing consumer-side interpretation. + +## 5. FFI Error Path Memory Leak +**Issue:** If `ArrowArrayStreamReader::from_raw` fails on an FFI stream exported by `sf_core`, the Arrow C Data Interface specification states that the release callback is *not* called. +**Impact:** If the driver doesn't handle the error, the `Box::into_raw` pointer leaks. +**Workaround in Driver:** We catch the error in `map_err`, reconstruct the `Box` using `Box::from_raw`, and explicitly `drop` it. +**Ideal Fix:** +- **Arrow Crate:** Provide a safer abstraction for FFI stream consumption that guarantees cleanup on failure. + +## 6. Timezone Handling in Arrow IPC +**Issue:** Snowflake's Arrow IPC responses include named timezones like `"UTC"` in the metadata. The Rust `arrow` crate requires the `"chrono-tz"` feature to parse these named timezones; otherwise, decoding fails. +**Impact:** Consumers of the ADBC driver (like `pyarrow`) receive invalid timezone errors. +**Workaround in Driver:** The driver enables the `"chrono-tz"` feature on its `arrow-array` dependency. +**Ideal Fix:** +- **sf_core:** Normalize timezone strings to standard offsets (e.g., `"+00:00"`) before exporting the FFI stream, removing the requirement for consumers to bundle heavy timezone databases just to parse `"UTC"`. + +## Summary +The current Rust ADBC driver performs significant "last-mile" data conversion (scaled integers, timestamp reconstruction, schema adjustment). If `sf_core` were to export canonical Arrow types (`Decimal128`, `Timestamp(unit, tz)`) directly, the `ConvertingReader` in the ADBC driver could be completely eliminated, resulting in a zero-copy, highly performant passthrough. From d524c866d5758492cfc261e164fc1472625cefb0 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Sat, 28 Mar 2026 17:37:32 -0400 Subject: [PATCH 47/76] fix(roborev): address all open review findings for precision and memory safety - Job 807/808: Clarify ns_to_unit truncation comment, fix unreachable branch in fraction builders, and add unit tests for clamped scale boundaries. - Job 809: Clamp precision parsed from metadata to 38, ensure identity Decimal128 path casts to exact target_type metadata, and add missing safety documentation for build_unchecked reinterpret. - Job 810: Apply Box::from_raw cleanup on failure across all four remaining FFI ArrowArrayStreamReader imports to prevent memory leaks, and update the limitations README to accurately describe the build_unchecked operation. --- rust/src/connection.rs | 16 ++- rust/src/get_objects.rs | 8 +- rust/src/statement.rs | 130 +++++++++++++++--- rust/validation/README_sf_core_limitations.md | 2 +- 4 files changed, 131 insertions(+), 25 deletions(-) diff --git a/rust/src/connection.rs b/rust/src/connection.rs index 78a215a..2b14744 100644 --- a/rust/src/connection.rs +++ b/rust/src/connection.rs @@ -134,7 +134,13 @@ impl Connection { let raw = Box::into_raw(exec_result.stream) as *mut arrow_array::ffi_stream::FFI_ArrowArrayStream; let mut reader = unsafe { arrow_array::ffi_stream::ArrowArrayStreamReader::from_raw(raw) } - .map_err(|e| Error::with_message_and_status(e.to_string(), Status::IO))?; + .map_err(|e| { + // Safety: Arrow's C Data Interface specifies that on failure, from_raw + // does NOT call the stream's release callback, so reconstructing the + // Box here is the only release path — no double-free risk. + drop(unsafe { Box::from_raw(raw) }); + Error::with_message_and_status(e.to_string(), Status::IO) + })?; use arrow_array::cast::AsArray; let batch = reader @@ -491,7 +497,13 @@ impl adbc_core::Connection for Connection { let raw = Box::into_raw(exec_result.stream) as *mut arrow_array::ffi_stream::FFI_ArrowArrayStream; let reader = unsafe { arrow_array::ffi_stream::ArrowArrayStreamReader::from_raw(raw) } - .map_err(|e| Error::with_message_and_status(e.to_string(), Status::IO))?; + .map_err(|e| { + // Safety: Arrow's C Data Interface specifies that on failure, from_raw + // does NOT call the stream's release callback, so reconstructing the + // Box here is the only release path — no double-free risk. + drop(unsafe { Box::from_raw(raw) }); + Error::with_message_and_status(e.to_string(), Status::IO) + })?; let mut fields: Vec = Vec::new(); for batch in reader { diff --git a/rust/src/get_objects.rs b/rust/src/get_objects.rs index 3cefe5f..4fa7421 100644 --- a/rust/src/get_objects.rs +++ b/rust/src/get_objects.rs @@ -104,7 +104,13 @@ fn run_query(conn: &Connection, sql: &str) -> Result>>> { let raw = Box::into_raw(exec.stream) as *mut arrow_array::ffi_stream::FFI_ArrowArrayStream; let reader = unsafe { arrow_array::ffi_stream::ArrowArrayStreamReader::from_raw(raw) } - .map_err(|e| Error::with_message_and_status(e.to_string(), Status::IO))?; + .map_err(|e| { + // Safety: Arrow's C Data Interface specifies that on failure, from_raw + // does NOT call the stream's release callback, so reconstructing the + // Box here is the only release path — no double-free risk. + drop(unsafe { Box::from_raw(raw) }); + Error::with_message_and_status(e.to_string(), Status::IO) + })?; let mut rows = Vec::new(); for batch_res in reader { diff --git a/rust/src/statement.rs b/rust/src/statement.rs index cd177df..9b6ad53 100644 --- a/rust/src/statement.rs +++ b/rust/src/statement.rs @@ -368,11 +368,15 @@ impl adbc_core::Statement for Statement { // release callback fires before the handle is reused. let raw = Box::into_raw(result.stream) as *mut arrow_array::ffi_stream::FFI_ArrowArrayStream; - if let Ok(reader) = - unsafe { arrow_array::ffi_stream::ArrowArrayStreamReader::from_raw(raw) } - { - for _ in reader {} // consume all batches to trigger release - } + let reader = unsafe { arrow_array::ffi_stream::ArrowArrayStreamReader::from_raw(raw) } + .map_err(|e| { + // Safety: Arrow's C Data Interface specifies that on failure, from_raw + // does NOT call the stream's release callback, so reconstructing the + // Box here is the only release path — no double-free risk. + drop(unsafe { Box::from_raw(raw) }); + Error::with_message_and_status(e.to_string(), Status::IO) + })?; + for _ in reader {} // consume all batches to trigger release total += result.rows_affected.unwrap_or(0); } } @@ -432,7 +436,13 @@ impl adbc_core::Statement for Statement { let raw = Box::into_raw(result.stream) as *mut arrow_array::ffi_stream::FFI_ArrowArrayStream; let reader = unsafe { arrow_array::ffi_stream::ArrowArrayStreamReader::from_raw(raw) } - .map_err(|e| Error::with_message_and_status(e.to_string(), Status::IO))?; + .map_err(|e| { + // Safety: Arrow's C Data Interface specifies that on failure, from_raw + // does NOT call the stream's release callback, so reconstructing the + // Box here is the only release path — no double-free risk. + drop(unsafe { Box::from_raw(raw) }); + Error::with_message_and_status(e.to_string(), Status::IO) + })?; // .schema() calls get_schema on the FFI stream without consuming any record batches. // Dropping the reader invokes the stream's release callback. Ok(adjust_schema(&reader.schema(), self.use_high_precision, self.timestamp_precision.time_unit()).as_ref().clone()) @@ -559,7 +569,8 @@ fn compute_target_type(field: &Field, use_high_precision: bool, ts_unit: TimeUni .metadata() .get("precision") .and_then(|s| s.parse().ok()) - .unwrap_or(38); + .unwrap_or(38) + .min(38); match logical_type { "FIXED" => { @@ -678,8 +689,8 @@ impl ConvertingReader { } } else if use_high_precision { match col.data_type() { - // Source is already Decimal128 — pass through as identity - DataType::Decimal128(_, _) => Ok(col.clone()), + // Source is already Decimal128 — cast to ensure target_type metadata is applied exactly + DataType::Decimal128(_, _) => arrow_cast::cast(col.as_ref(), target_type), // Source is Int64 (scaled integer) — convert to Decimal128(precision, scale) // following Go's integerToDecimal128: cast Int64 → Decimal128(20,0), // then reinterpret as Decimal128(precision, scale). @@ -689,6 +700,10 @@ impl ConvertingReader { &DataType::Decimal128(20, 0), )?; let data = intermediate.to_data(); + // Safety: Decimal128 and Decimal128(20,0) share identical 128-bit + // buffer layouts, so this reinterpret is memory-safe. + // Values that exceed target_type's precision are not validated here + // and will silently overflow the declared precision. let retyped = unsafe { data.into_builder() .data_type(target_type.clone()) @@ -851,8 +866,7 @@ fn ns_to_unit(ns: i128, unit: TimeUnit) -> i64 { TimeUnit::Second => ns / 1_000_000_000, TimeUnit::Millisecond => ns / 1_000_000, TimeUnit::Microsecond => ns / 1_000, - // Intentional truncation: ns timestamps outside i64 range silently wrap. - // Callers that need strict overflow detection should pass check_overflow=true. + // Intentional truncation: ns outside i64::MIN..=i64::MAX wraps. Use check_overflow=true in the caller to reject out-of-range values *before* this cast is reached. TimeUnit::Nanosecond => ns, }; val as i64 @@ -873,11 +887,7 @@ fn build_timestamp_from_epoch_fraction( let len = epoch.len(); let scale = scale.clamp(0, 9); - let frac_to_ns: i128 = if scale <= 9 { - 10i128.pow((9 - scale) as u32) - } else { - 1 - }; + let frac_to_ns: i128 = 10i128.pow((9 - scale) as u32); let mut values: Vec> = Vec::with_capacity(len); for i in 0..len { @@ -963,11 +973,7 @@ fn build_timestamp_tz_3field( let len = epoch.len(); let scale = scale.clamp(0, 9); - let frac_to_ns: i128 = if scale <= 9 { - 10i128.pow((9 - scale) as u32) - } else { - 1 - }; + let frac_to_ns: i128 = 10i128.pow((9 - scale) as u32); let mut values: Vec> = Vec::with_capacity(len); for i in 0..len { @@ -1784,4 +1790,86 @@ mod tests { assert_eq!(ts.value(0), epoch_us, "year 9999 should round-trip as microseconds"); } + #[test] + fn test_build_timestamp_from_epoch_fraction_negative_scale_clamps() { + let epoch = arrow_array::Int64Array::from(vec![0i64]); + let fraction = arrow_array::Int32Array::from(vec![0i32]); + let fields = vec![ + Field::new("epoch", DataType::Int64, true), + Field::new("fraction", DataType::Int32, true), + ]; + let struct_arr = arrow_array::StructArray::from(vec![ + (Arc::new(fields[0].clone()), Arc::new(epoch.clone()) as ArrayRef), + (Arc::new(fields[1].clone()), Arc::new(fraction.clone()) as ArrayRef), + ]); + let result = build_timestamp_from_epoch_fraction( + &epoch, &fraction, &struct_arr, -1, false, + DataType::Timestamp(TimeUnit::Nanosecond, None), + ); + assert!(result.is_ok(), "scale=-1 should not panic"); + } + + #[test] + fn test_build_timestamp_from_epoch_fraction_oversized_scale_clamps() { + let epoch = arrow_array::Int64Array::from(vec![0i64]); + let fraction = arrow_array::Int32Array::from(vec![0i32]); + let fields = vec![ + Field::new("epoch", DataType::Int64, true), + Field::new("fraction", DataType::Int32, true), + ]; + let struct_arr = arrow_array::StructArray::from(vec![ + (Arc::new(fields[0].clone()), Arc::new(epoch.clone()) as ArrayRef), + (Arc::new(fields[1].clone()), Arc::new(fraction.clone()) as ArrayRef), + ]); + let result = build_timestamp_from_epoch_fraction( + &epoch, &fraction, &struct_arr, 10, false, + DataType::Timestamp(TimeUnit::Nanosecond, None), + ); + assert!(result.is_ok(), "scale=10 should not panic"); + } + + #[test] + fn test_build_timestamp_tz_3field_negative_scale_clamps() { + let epoch = arrow_array::Int64Array::from(vec![0i64]); + let fraction = arrow_array::Int32Array::from(vec![0i32]); + let tzoffset = arrow_array::Int32Array::from(vec![1440i32]); + let fields = vec![ + Field::new("epoch", DataType::Int64, true), + Field::new("fraction", DataType::Int32, true), + Field::new("tzoffset", DataType::Int32, true), + ]; + let struct_arr = arrow_array::StructArray::from(vec![ + (Arc::new(fields[0].clone()), Arc::new(epoch.clone()) as ArrayRef), + (Arc::new(fields[1].clone()), Arc::new(fraction.clone()) as ArrayRef), + (Arc::new(fields[2].clone()), Arc::new(tzoffset.clone()) as ArrayRef), + ]); + let result = build_timestamp_tz_3field( + &epoch, &fraction, &tzoffset, &struct_arr, -1, false, + DataType::Timestamp(TimeUnit::Nanosecond, Some(Arc::from("UTC"))), + ); + assert!(result.is_ok(), "scale=-1 should not panic"); + } + + #[test] + fn test_build_timestamp_tz_3field_oversized_scale_clamps() { + let epoch = arrow_array::Int64Array::from(vec![0i64]); + let fraction = arrow_array::Int32Array::from(vec![0i32]); + let tzoffset = arrow_array::Int32Array::from(vec![1440i32]); + let fields = vec![ + Field::new("epoch", DataType::Int64, true), + Field::new("fraction", DataType::Int32, true), + Field::new("tzoffset", DataType::Int32, true), + ]; + let struct_arr = arrow_array::StructArray::from(vec![ + (Arc::new(fields[0].clone()), Arc::new(epoch.clone()) as ArrayRef), + (Arc::new(fields[1].clone()), Arc::new(fraction.clone()) as ArrayRef), + (Arc::new(fields[2].clone()), Arc::new(tzoffset.clone()) as ArrayRef), + ]); + let result = build_timestamp_tz_3field( + &epoch, &fraction, &tzoffset, &struct_arr, 10, false, + DataType::Timestamp(TimeUnit::Nanosecond, Some(Arc::from("UTC"))), + ); + assert!(result.is_ok(), "scale=10 should not panic"); + } + } diff --git a/rust/validation/README_sf_core_limitations.md b/rust/validation/README_sf_core_limitations.md index 5e8fb49..1d3c2ec 100644 --- a/rust/validation/README_sf_core_limitations.md +++ b/rust/validation/README_sf_core_limitations.md @@ -30,7 +30,7 @@ This document serves as a reference for future improvements that should ideally ## 4. Scaled Integers (`FIXED` type) **Issue:** `sf_core` returns `FIXED` (e.g., `NUMBER(10,3)`) columns as raw `Int64` buffers representing the scaled integer (e.g., `12345` for `12.345`). The Arrow FFI stream schema only reports `Int64` with custom metadata (`logicalType: "FIXED"`, `scale: "3"`). **Impact:** The ADBC driver has to implement a complex `ConvertingReader` to interpret this metadata and manually cast `Int64` to `Float64` (dividing by `10^scale`) or `Decimal128`. -**Workaround in Driver:** We intercept the Arrow FFI stream, read the metadata, and perform a secondary pass using `arrow_cast::cast` + manual division (for `Float64`) or unsafe metadata rewriting (`build_unchecked`) for `Decimal128`. +**Workaround in Driver:** We intercept the Arrow FFI stream, read the metadata, and perform a secondary pass using `arrow_cast::cast` + manual division (for `Float64`) or unchecked precision reinterpretation via `build_unchecked` (values that exceed `target_type`'s precision are not rejected) for `Decimal128`. **Ideal Fix:** - **sf_core:** Automatically apply the scale. If `sf_core` detects a `FIXED` type with `scale > 0`, it should export the FFI stream as `Decimal128` natively. Arrow already supports `Decimal128Type`, which perfectly represents Snowflake's scaled integers without needing consumer-side interpretation. From 4f1ed30dd6b1d92f96fcce01faf052b81ee5d74d Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Sat, 28 Mar 2026 17:43:09 -0400 Subject: [PATCH 48/76] fix(roborev): drain loop continues on FFI error; scale clamp tests assert values In execute_update drain loop, use match/continue instead of ? so that subsequent result streams are still drained when one fails (preventing cascading resource leaks). Scale clamp tests now use non-zero epoch=1/fraction=5 and assert the specific nanosecond values produced by the clamped computation rather than only checking for no-panic. --- rust/src/statement.rs | 54 ++++++++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/rust/src/statement.rs b/rust/src/statement.rs index 9b6ad53..7c85c9b 100644 --- a/rust/src/statement.rs +++ b/rust/src/statement.rs @@ -368,15 +368,19 @@ impl adbc_core::Statement for Statement { // release callback fires before the handle is reused. let raw = Box::into_raw(result.stream) as *mut arrow_array::ffi_stream::FFI_ArrowArrayStream; - let reader = unsafe { arrow_array::ffi_stream::ArrowArrayStreamReader::from_raw(raw) } - .map_err(|e| { + match unsafe { arrow_array::ffi_stream::ArrowArrayStreamReader::from_raw(raw) } { + Ok(reader) => { + for _ in reader {} // consume all batches to trigger release + } + Err(e) => { // Safety: Arrow's C Data Interface specifies that on failure, from_raw // does NOT call the stream's release callback, so reconstructing the // Box here is the only release path — no double-free risk. drop(unsafe { Box::from_raw(raw) }); - Error::with_message_and_status(e.to_string(), Status::IO) - })?; - for _ in reader {} // consume all batches to trigger release + // Log the error but continue the loop to drain remaining streams + eprintln!("Warning: failed to initialize FFI reader for draining: {}", e); + } + } total += result.rows_affected.unwrap_or(0); } } @@ -1792,8 +1796,8 @@ mod tests { #[test] fn test_build_timestamp_from_epoch_fraction_negative_scale_clamps() { - let epoch = arrow_array::Int64Array::from(vec![0i64]); - let fraction = arrow_array::Int32Array::from(vec![0i32]); + let epoch = arrow_array::Int64Array::from(vec![1i64]); + let fraction = arrow_array::Int32Array::from(vec![5i32]); let fields = vec![ Field::new("epoch", DataType::Int64, true), Field::new("fraction", DataType::Int32, true), @@ -1805,14 +1809,17 @@ mod tests { let result = build_timestamp_from_epoch_fraction( &epoch, &fraction, &struct_arr, -1, false, DataType::Timestamp(TimeUnit::Nanosecond, None), - ); - assert!(result.is_ok(), "scale=-1 should not panic"); + ).expect("should not panic"); + let ts = result.as_any().downcast_ref::().unwrap(); + // scale -1 clamps to 0. frac_to_ns = 10^9. + // ns = 1 * 10^9 + 5 * 10^9 = 6000000000. + assert_eq!(ts.value(0), 6_000_000_000); } #[test] fn test_build_timestamp_from_epoch_fraction_oversized_scale_clamps() { - let epoch = arrow_array::Int64Array::from(vec![0i64]); - let fraction = arrow_array::Int32Array::from(vec![0i32]); + let epoch = arrow_array::Int64Array::from(vec![1i64]); + let fraction = arrow_array::Int32Array::from(vec![5i32]); let fields = vec![ Field::new("epoch", DataType::Int64, true), Field::new("fraction", DataType::Int32, true), @@ -1824,14 +1831,17 @@ mod tests { let result = build_timestamp_from_epoch_fraction( &epoch, &fraction, &struct_arr, 10, false, DataType::Timestamp(TimeUnit::Nanosecond, None), - ); - assert!(result.is_ok(), "scale=10 should not panic"); + ).expect("should not panic"); + let ts = result.as_any().downcast_ref::().unwrap(); + // scale 10 clamps to 9. frac_to_ns = 10^0 = 1. + // ns = 1 * 10^9 + 5 * 1 = 1000000005. + assert_eq!(ts.value(0), 1_000_000_005); } #[test] fn test_build_timestamp_tz_3field_negative_scale_clamps() { - let epoch = arrow_array::Int64Array::from(vec![0i64]); - let fraction = arrow_array::Int32Array::from(vec![0i32]); + let epoch = arrow_array::Int64Array::from(vec![1i64]); + let fraction = arrow_array::Int32Array::from(vec![5i32]); let tzoffset = arrow_array::Int32Array::from(vec![1440i32]); let fields = vec![ Field::new("epoch", DataType::Int64, true), @@ -1846,14 +1856,15 @@ mod tests { let result = build_timestamp_tz_3field( &epoch, &fraction, &tzoffset, &struct_arr, -1, false, DataType::Timestamp(TimeUnit::Nanosecond, Some(Arc::from("UTC"))), - ); - assert!(result.is_ok(), "scale=-1 should not panic"); + ).expect("should not panic"); + let ts = result.as_any().downcast_ref::().unwrap(); + assert_eq!(ts.value(0), 6_000_000_000); } #[test] fn test_build_timestamp_tz_3field_oversized_scale_clamps() { - let epoch = arrow_array::Int64Array::from(vec![0i64]); - let fraction = arrow_array::Int32Array::from(vec![0i32]); + let epoch = arrow_array::Int64Array::from(vec![1i64]); + let fraction = arrow_array::Int32Array::from(vec![5i32]); let tzoffset = arrow_array::Int32Array::from(vec![1440i32]); let fields = vec![ Field::new("epoch", DataType::Int64, true), @@ -1868,8 +1879,9 @@ mod tests { let result = build_timestamp_tz_3field( &epoch, &fraction, &tzoffset, &struct_arr, 10, false, DataType::Timestamp(TimeUnit::Nanosecond, Some(Arc::from("UTC"))), - ); - assert!(result.is_ok(), "scale=10 should not panic"); + ).expect("should not panic"); + let ts = result.as_any().downcast_ref::().unwrap(); + assert_eq!(ts.value(0), 1_000_000_005); } } From 44d6a10075abb63db5c1b02e86572d7d4074a31b Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Sat, 28 Mar 2026 22:52:02 -0400 Subject: [PATCH 49/76] fix(roborev 812): use proper log facade instead of eprintln Replace direct eprintln! writing to stderr in the execute_update drain loop with log::warn! to respect the application's configured logging framework. Add 'log' crate to dependencies. --- rust/Cargo.lock | 1 + rust/Cargo.toml | 1 + rust/src/statement.rs | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index a40858a..1fa4e4a 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -12,6 +12,7 @@ dependencies = [ "arrow-buffer 57.3.0", "arrow-cast 57.3.0", "arrow-schema 57.3.0", + "log", "sf_core", "tokio", ] diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 2695e0d..9c5608c 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -36,4 +36,5 @@ arrow-array = { version = "57.3.0", default-features = false, features = ["ffi" arrow-buffer = { version = "57.3.0", default-features = false } arrow-schema = { version = "57.3.0", default-features = false } arrow-cast = { version = "57.3.0", default-features = false } +log = "0.4.22" tokio = { version = "1", features = ["rt-multi-thread"] } diff --git a/rust/src/statement.rs b/rust/src/statement.rs index 7c85c9b..7447894 100644 --- a/rust/src/statement.rs +++ b/rust/src/statement.rs @@ -378,7 +378,7 @@ impl adbc_core::Statement for Statement { // Box here is the only release path — no double-free risk. drop(unsafe { Box::from_raw(raw) }); // Log the error but continue the loop to drain remaining streams - eprintln!("Warning: failed to initialize FFI reader for draining: {}", e); + log::warn!("failed to initialize FFI reader for draining: {}", e); } } total += result.rows_affected.unwrap_or(0); From eaadfe402f4a2cba029a0158ebd7ee206b957634 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Mon, 30 Mar 2026 12:56:19 -0400 Subject: [PATCH 50/76] fix(rust): Add OPENSSL_DIR for windows builds --- rust/ci/scripts/pre-build.sh | 1 + 1 file changed, 1 insertion(+) create mode 100755 rust/ci/scripts/pre-build.sh diff --git a/rust/ci/scripts/pre-build.sh b/rust/ci/scripts/pre-build.sh new file mode 100755 index 0000000..32479cc --- /dev/null +++ b/rust/ci/scripts/pre-build.sh @@ -0,0 +1 @@ +if [[ "$2" == "windows" ]]; then echo "OPENSSL_DIR=C:\Program Files\OpenSSL-Win64" > .env.build; fi From 97aefc04ba3d842d1fc8ba9d6a350a7438b185d4 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Mon, 30 Mar 2026 13:02:10 -0400 Subject: [PATCH 51/76] fix(rust): Quote OPENSSL_DIR value in .env.build for Windows --- rust/ci/scripts/pre-build.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/rust/ci/scripts/pre-build.sh b/rust/ci/scripts/pre-build.sh index 32479cc..f19d540 100755 --- a/rust/ci/scripts/pre-build.sh +++ b/rust/ci/scripts/pre-build.sh @@ -1 +1,7 @@ -if [[ "$2" == "windows" ]]; then echo "OPENSSL_DIR=C:\Program Files\OpenSSL-Win64" > .env.build; fi +#!/usr/bin/env bash + +set -ex + +if [[ "$2" == "windows" ]]; then + echo "OPENSSL_DIR='C:\Program Files\OpenSSL-Win64'" > .env.build +fi From c68f03f938c1af4be80098d2c16fc83041bf415b Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Mon, 30 Mar 2026 13:07:49 -0400 Subject: [PATCH 52/76] ci(rust): Disable fail-fast in rust_test.yaml to debug Windows build --- .github/workflows/rust_test.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/rust_test.yaml b/.github/workflows/rust_test.yaml index c5f6ef2..22fd599 100644 --- a/.github/workflows/rust_test.yaml +++ b/.github/workflows/rust_test.yaml @@ -65,7 +65,7 @@ jobs: name: "Test/${{ matrix.platform }}_${{ matrix.arch }}" runs-on: ${{ matrix.runner }} strategy: - fail-fast: true + fail-fast: false matrix: include: - { platform: linux, arch: amd64, runner: ubuntu-latest } @@ -189,7 +189,7 @@ jobs: name: "Validate/${{ matrix.platform }}_${{ matrix.arch }}" runs-on: ${{ matrix.runner }} strategy: - fail-fast: true + fail-fast: false matrix: include: # I think we only need to test one platform, but we can change that later @@ -333,7 +333,7 @@ jobs: needs: test runs-on: ${{ matrix.runner }} strategy: - fail-fast: true + fail-fast: false matrix: include: - { platform: linux, arch: amd64, runner: ubuntu-latest } From 989324149c2cedf3b2d21e04f617a069666774a0 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Mon, 30 Mar 2026 13:38:34 -0400 Subject: [PATCH 53/76] fix(rust): URL-decode username and password from Snowflake URI When using a JWT as a password in the DSN, it often contains characters like '=' for padding, which are URL-encoded as '%3D'. The Go driver (gosnowflake) implicitly url-decodes these values, but the Rust ADBC driver was parsing the raw encoded string, causing 'Incorrect username or password' authentication failures. This aligns the ADBC Rust implementation with the Go driver by decoding percent-encoded values in the username and password fields. --- rust/Cargo.lock | 51 +++++++++++++++++++++++++++++++++++++++++++- rust/Cargo.toml | 5 +++++ rust/src/database.rs | 16 +++++++++----- 3 files changed, 66 insertions(+), 6 deletions(-) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 1fa4e4a..208c79c 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -12,9 +12,13 @@ dependencies = [ "arrow-buffer 57.3.0", "arrow-cast 57.3.0", "arrow-schema 57.3.0", + "env_logger 0.10.2", "log", + "percent-encoding", "sf_core", "tokio", + "tracing", + "tracing-subscriber", ] [[package]] @@ -1486,6 +1490,19 @@ dependencies = [ "regex", ] +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + [[package]] name = "env_logger" version = "0.11.10" @@ -1874,6 +1891,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -1965,6 +1988,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "humantime" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" + [[package]] name = "hyper" version = "0.14.32" @@ -2254,6 +2283,17 @@ dependencies = [ "serde", ] +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -3142,7 +3182,7 @@ version = "0.1.0" source = "git+https://github.com/snowflakedb/universal-driver?rev=66a816ec1d1adda899bd2607f5c83e7dd5838ad5#66a816ec1d1adda899bd2607f5c83e7dd5838ad5" dependencies = [ "clap", - "env_logger", + "env_logger 0.11.10", "log", "prost 0.12.6", "prost-build", @@ -4038,6 +4078,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "1.0.69" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 9c5608c..e224d0c 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -38,3 +38,8 @@ arrow-schema = { version = "57.3.0", default-features = false } arrow-cast = { version = "57.3.0", default-features = false } log = "0.4.22" tokio = { version = "1", features = ["rt-multi-thread"] } +percent-encoding = "2.3.2" +[dev-dependencies] +env_logger = "0.10" +tracing = "0.1.44" +tracing-subscriber = "0.3.23" diff --git a/rust/src/database.rs b/rust/src/database.rs index 3e1ab63..dc985e0 100644 --- a/rust/src/database.rs +++ b/rust/src/database.rs @@ -36,6 +36,8 @@ use sf_core::config::settings::Setting; use crate::connection::Connection; use crate::driver::{Inner, TimestampPrecision}; +use percent_encoding::percent_decode_str; + /// Convert an ADBC OptionDatabase key + OptionValue into an sf_core (param_name, Setting) pair. /// Returns None for the "uri" key (handled by apply_uri separately). /// Returns Err for keys with invalid values (e.g. non-numeric port). @@ -335,20 +337,24 @@ impl Database { if let Some(info) = user_info { if let Some(colon) = info.find(':') { - let user = &info[..colon]; - let pass = &info[colon + 1..]; + let user = percent_decode_str(&info[..colon]).decode_utf8_lossy(); + let pass = percent_decode_str(&info[colon + 1..]).decode_utf8_lossy(); if !user.is_empty() { self.set_option( OptionDatabase::Username, - OptionValue::String(user.to_string()), + OptionValue::String(user.into_owned()), )?; } self.set_option( OptionDatabase::Password, - OptionValue::String(pass.to_string()), + OptionValue::String(pass.into_owned()), )?; } else if !info.is_empty() { - self.set_option(OptionDatabase::Username, OptionValue::String(info))?; + let user = percent_decode_str(&info).decode_utf8_lossy(); + self.set_option( + OptionDatabase::Username, + OptionValue::String(user.into_owned()) + )?; } } From 660b61fa0123c4856a3e12b561df77047aad1d7b Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Mon, 30 Mar 2026 14:24:06 -0400 Subject: [PATCH 54/76] cargo fmt --- rust/src/connection.rs | 12 +- rust/src/database.rs | 4 +- rust/src/driver.rs | 2 +- rust/src/get_objects.rs | 61 +++--- rust/src/ingest.rs | 9 +- rust/src/statement.rs | 391 ++++++++++++++++++++++++++------------ rust/tests/integration.rs | 4 +- 7 files changed, 312 insertions(+), 171 deletions(-) diff --git a/rust/src/connection.rs b/rust/src/connection.rs index 2b14744..d5eddbf 100644 --- a/rust/src/connection.rs +++ b/rust/src/connection.rs @@ -807,11 +807,7 @@ mod tests { #[test] fn snowflake_type_timestamp_ntz_scale6_with_ns_unit_returns_us() { assert_eq!( - snowflake_type_to_arrow( - "TIMESTAMP_NTZ(6)", - true, - arrow_schema::TimeUnit::Nanosecond - ), + snowflake_type_to_arrow("TIMESTAMP_NTZ(6)", true, arrow_schema::TimeUnit::Nanosecond), DataType::Timestamp(arrow_schema::TimeUnit::Microsecond, None) ); } @@ -831,11 +827,7 @@ mod tests { #[test] fn snowflake_type_timestamp_ltz_scale6_with_ns_unit_returns_us() { assert_eq!( - snowflake_type_to_arrow( - "TIMESTAMP_LTZ(6)", - true, - arrow_schema::TimeUnit::Nanosecond - ), + snowflake_type_to_arrow("TIMESTAMP_LTZ(6)", true, arrow_schema::TimeUnit::Nanosecond), DataType::Timestamp(arrow_schema::TimeUnit::Microsecond, Some("UTC".into())) ); } diff --git a/rust/src/database.rs b/rust/src/database.rs index dc985e0..72c6f0a 100644 --- a/rust/src/database.rs +++ b/rust/src/database.rs @@ -352,8 +352,8 @@ impl Database { } else if !info.is_empty() { let user = percent_decode_str(&info).decode_utf8_lossy(); self.set_option( - OptionDatabase::Username, - OptionValue::String(user.into_owned()) + OptionDatabase::Username, + OptionValue::String(user.into_owned()), )?; } } diff --git a/rust/src/driver.rs b/rust/src/driver.rs index 545cdf3..a0766d2 100644 --- a/rust/src/driver.rs +++ b/rust/src/driver.rs @@ -24,9 +24,9 @@ use std::sync::Arc; use adbc_core::{ + Optionable, error::{Error, Result, Status}, options::{OptionDatabase, OptionValue}, - Optionable, }; use arrow_schema::TimeUnit; use sf_core::apis::database_driver_v1::DatabaseDriverV1; diff --git a/rust/src/get_objects.rs b/rust/src/get_objects.rs index 4fa7421..1735f45 100644 --- a/rust/src/get_objects.rs +++ b/rust/src/get_objects.rs @@ -399,36 +399,37 @@ fn collect( }; let key = (cat.clone(), sch.clone()); if let Some(tables) = tables_by_key.get_mut(&key) - && let Some(table) = tables.iter_mut().find(|t| &t.name == tbl) { - let nullable_str = row[7].clone(); - let nullable_int = nullable_str.as_deref().map(|s| { - if s.eq_ignore_ascii_case("YES") { - 1i16 - } else { - 0 - } - }); - let ordinal = { - let c = col_counter - .entry((cat.clone(), sch.clone(), tbl.clone())) - .or_insert(0); - *c += 1; - *c - }; - table.columns.push(ColEntry { - name: col_name.clone(), - ordinal_position: ordinal, - remarks: row[5].clone(), - xdbc_type_name: row[6].clone(), - xdbc_column_size: row[8].as_deref().and_then(|s| s.parse().ok()), - xdbc_char_octet_length: row[9].as_deref().and_then(|s| s.parse().ok()), - xdbc_decimal_digits: row[10].as_deref().and_then(|s| s.parse().ok()), - xdbc_num_prec_radix: row[11].as_deref().and_then(|s| s.parse().ok()), - xdbc_nullable: nullable_int, - xdbc_is_nullable: nullable_str, - xdbc_datetime_sub: row[12].as_deref().and_then(|s| s.parse().ok()), - }); - } + && let Some(table) = tables.iter_mut().find(|t| &t.name == tbl) + { + let nullable_str = row[7].clone(); + let nullable_int = nullable_str.as_deref().map(|s| { + if s.eq_ignore_ascii_case("YES") { + 1i16 + } else { + 0 + } + }); + let ordinal = { + let c = col_counter + .entry((cat.clone(), sch.clone(), tbl.clone())) + .or_insert(0); + *c += 1; + *c + }; + table.columns.push(ColEntry { + name: col_name.clone(), + ordinal_position: ordinal, + remarks: row[5].clone(), + xdbc_type_name: row[6].clone(), + xdbc_column_size: row[8].as_deref().and_then(|s| s.parse().ok()), + xdbc_char_octet_length: row[9].as_deref().and_then(|s| s.parse().ok()), + xdbc_decimal_digits: row[10].as_deref().and_then(|s| s.parse().ok()), + xdbc_num_prec_radix: row[11].as_deref().and_then(|s| s.parse().ok()), + xdbc_nullable: nullable_int, + xdbc_is_nullable: nullable_str, + xdbc_datetime_sub: row[12].as_deref().and_then(|s| s.parse().ok()), + }); + } } Ok(assemble(catalog_names, schemas_by_cat, tables_by_key)) diff --git a/rust/src/ingest.rs b/rust/src/ingest.rs index af61602..37333a8 100644 --- a/rust/src/ingest.rs +++ b/rust/src/ingest.rs @@ -185,7 +185,7 @@ fn to_sf_ddl(dt: &DataType) -> Result { return Err(Error::with_message_and_status( format!("ingest of nested type {dt:?} is not yet supported"), Status::NotImplemented, - )) + )); } other => { @@ -406,9 +406,10 @@ fn value_to_sql(arr: &dyn Array, row: usize, dt: &DataType) -> Result { } if let Some(a) = arr.as_any().downcast_ref::() - && let DataType::Decimal128(_, scale) = dt { - return Ok(decimal128_to_str(a.value(row), *scale)); - } + && let DataType::Decimal128(_, scale) = dt + { + return Ok(decimal128_to_str(a.value(row), *scale)); + } Err(Error::with_message_and_status( format!("unsupported ingest value type: {dt:?}"), diff --git a/rust/src/statement.rs b/rust/src/statement.rs index 7447894..fee9e8a 100644 --- a/rust/src/statement.rs +++ b/rust/src/statement.rs @@ -226,15 +226,15 @@ impl Statement { } } - let schema = result_schema.unwrap_or_else(|| Arc::new(Schema::empty())); - Ok(Box::new(ConvertingReader::new( - ConcatReader { - batches: all_batches.into_iter(), - schema, - }, - self.use_high_precision, - self.timestamp_precision.time_unit(), - ))) + let schema = result_schema.unwrap_or_else(|| Arc::new(Schema::empty())); + Ok(Box::new(ConvertingReader::new( + ConcatReader { + batches: all_batches.into_iter(), + schema, + }, + self.use_high_precision, + self.timestamp_precision.time_unit(), + ))) } fn apply_query_tag(&self) -> Result<()> { @@ -321,15 +321,19 @@ impl adbc_core::Statement for Statement { // to ArrowArrayStreamReader. The C ABI layout is stable per the Arrow C Data Interface. let raw = Box::into_raw(result.stream) as *mut arrow_array::ffi_stream::FFI_ArrowArrayStream; - let reader = unsafe { arrow_array::ffi_stream::ArrowArrayStreamReader::from_raw(raw) } - .map_err(|e| { - // Safety: Arrow's C Data Interface specifies that on failure, from_raw - // does NOT call the stream's release callback, so reconstructing the - // Box here is the only release path — no double-free risk. - drop(unsafe { Box::from_raw(raw) }); - Error::with_message_and_status(e.to_string(), Status::IO) - })?; - Ok(Box::new(ConvertingReader::new(reader, self.use_high_precision, self.timestamp_precision.time_unit()))) + let reader = unsafe { arrow_array::ffi_stream::ArrowArrayStreamReader::from_raw(raw) } + .map_err(|e| { + // Safety: Arrow's C Data Interface specifies that on failure, from_raw + // does NOT call the stream's release callback, so reconstructing the + // Box here is the only release path — no double-free risk. + drop(unsafe { Box::from_raw(raw) }); + Error::with_message_and_status(e.to_string(), Status::IO) + })?; + Ok(Box::new(ConvertingReader::new( + reader, + self.use_high_precision, + self.timestamp_precision.time_unit(), + ))) } fn execute_update(&mut self) -> Result> { @@ -368,7 +372,8 @@ impl adbc_core::Statement for Statement { // release callback fires before the handle is reused. let raw = Box::into_raw(result.stream) as *mut arrow_array::ffi_stream::FFI_ArrowArrayStream; - match unsafe { arrow_array::ffi_stream::ArrowArrayStreamReader::from_raw(raw) } { + match unsafe { arrow_array::ffi_stream::ArrowArrayStreamReader::from_raw(raw) } + { Ok(reader) => { for _ in reader {} // consume all batches to trigger release } @@ -447,9 +452,15 @@ impl adbc_core::Statement for Statement { drop(unsafe { Box::from_raw(raw) }); Error::with_message_and_status(e.to_string(), Status::IO) })?; - // .schema() calls get_schema on the FFI stream without consuming any record batches. - // Dropping the reader invokes the stream's release callback. - Ok(adjust_schema(&reader.schema(), self.use_high_precision, self.timestamp_precision.time_unit()).as_ref().clone()) + // .schema() calls get_schema on the FFI stream without consuming any record batches. + // Dropping the reader invokes the stream's release callback. + Ok(adjust_schema( + &reader.schema(), + self.use_high_precision, + self.timestamp_precision.time_unit(), + ) + .as_ref() + .clone()) } fn execute_partitions(&mut self) -> Result { @@ -647,12 +658,7 @@ impl ConvertingReader { let logical_types: Vec = orig_schema .fields() .iter() - .map(|f| { - f.metadata() - .get("logicalType") - .cloned() - .unwrap_or_default() - }) + .map(|f| f.metadata().get("logicalType").cloned().unwrap_or_default()) .collect(); let scales: Vec = orig_schema .fields() @@ -699,10 +705,8 @@ impl ConvertingReader { // following Go's integerToDecimal128: cast Int64 → Decimal128(20,0), // then reinterpret as Decimal128(precision, scale). _ => { - let intermediate = arrow_cast::cast( - col.as_ref(), - &DataType::Decimal128(20, 0), - )?; + let intermediate = + arrow_cast::cast(col.as_ref(), &DataType::Decimal128(20, 0))?; let data = intermediate.to_data(); // Safety: Decimal128 and Decimal128(20,0) share identical 128-bit // buffer layouts, so this reinterpret is memory-safe. @@ -742,13 +746,9 @@ impl ConvertingReader { "TIME" => arrow_cast::cast(col.as_ref(), target_type), "REAL" => arrow_cast::cast(col.as_ref(), &DataType::Float64), "TIMESTAMP_NTZ" => convert_timestamp_ntz(col, scale, ts_unit, check_overflow, None), - "TIMESTAMP_LTZ" => convert_timestamp_ntz( - col, - scale, - ts_unit, - check_overflow, - Some(Arc::from("UTC")), - ), + "TIMESTAMP_LTZ" => { + convert_timestamp_ntz(col, scale, ts_unit, check_overflow, Some(Arc::from("UTC"))) + } "TIMESTAMP_TZ" => convert_timestamp_tz(col, scale, ts_unit, check_overflow), _ => match col.data_type() { DataType::Int8 | DataType::Int16 | DataType::Int32 => { @@ -783,10 +783,9 @@ fn convert_timestamp_ntz( } DataType::Struct(_) => { use arrow_array::{Int32Array, Int64Array, StructArray}; - let struct_arr = col - .as_any() - .downcast_ref::() - .ok_or_else(|| arrow_schema::ArrowError::CastError("expected StructArray".into()))?; + let struct_arr = col.as_any().downcast_ref::().ok_or_else(|| { + arrow_schema::ArrowError::CastError("expected StructArray".into()) + })?; let epoch = struct_arr .column(0) .as_any() @@ -802,7 +801,14 @@ fn convert_timestamp_ntz( arrow_schema::ArrowError::CastError("expected Int32 fraction".into()) })?; - build_timestamp_from_epoch_fraction(epoch, fraction, struct_arr, scale, check_overflow, target) + build_timestamp_from_epoch_fraction( + epoch, + fraction, + struct_arr, + scale, + check_overflow, + target, + ) } _ => arrow_cast::cast(col.as_ref(), &target), } @@ -819,10 +825,9 @@ fn convert_timestamp_tz( let unit = timestamp_target_unit(scale, ts_unit); let target = DataType::Timestamp(unit, Some(Arc::from("UTC"))); - let struct_arr = col - .as_any() - .downcast_ref::() - .ok_or_else(|| arrow_schema::ArrowError::CastError("expected StructArray for TIMESTAMP_TZ".into()))?; + let struct_arr = col.as_any().downcast_ref::().ok_or_else(|| { + arrow_schema::ArrowError::CastError("expected StructArray for TIMESTAMP_TZ".into()) + })?; let num_fields = struct_arr.num_columns(); let epoch = struct_arr @@ -851,7 +856,15 @@ fn convert_timestamp_tz( .downcast_ref::() .ok_or_else(|| arrow_schema::ArrowError::CastError("expected Int32 timezone".into()))?; - build_timestamp_tz_3field(epoch, fraction, tzoffset, struct_arr, scale, check_overflow, target) + build_timestamp_tz_3field( + epoch, + fraction, + tzoffset, + struct_arr, + scale, + check_overflow, + target, + ) } } @@ -898,8 +911,8 @@ fn build_timestamp_from_epoch_fraction( if struct_arr.is_null(i) { values.push(None); } else { - let ns: i128 = epoch.value(i) as i128 * 1_000_000_000 - + fraction.value(i) as i128 * frac_to_ns; + let ns: i128 = + epoch.value(i) as i128 * 1_000_000_000 + fraction.value(i) as i128 * frac_to_ns; if check_overflow { check_ns_overflow(ns)?; } @@ -940,7 +953,8 @@ fn build_timestamp_tz_2field( let tz_offset_minutes: i128 = (tzoffset.value(i) as i128) - 1440; let tz_offset_ns: i128 = tz_offset_minutes * 60 * 1_000_000_000; - let epoch_ns: i128 = epoch.value(i) as i128 * 10i128.pow((9u32).saturating_sub(scale.clamp(0, 9) as u32)); + let epoch_ns: i128 = epoch.value(i) as i128 + * 10i128.pow((9u32).saturating_sub(scale.clamp(0, 9) as u32)); let utc_ns = epoch_ns - tz_offset_ns; if check_overflow { @@ -987,8 +1001,8 @@ fn build_timestamp_tz_3field( let tz_offset_minutes: i128 = (tzoffset.value(i) as i128) - 1440; let tz_offset_ns: i128 = tz_offset_minutes * 60 * 1_000_000_000; - let epoch_ns: i128 = epoch.value(i) as i128 * 1_000_000_000 - + fraction.value(i) as i128 * frac_to_ns; + let epoch_ns: i128 = + epoch.value(i) as i128 * 1_000_000_000 + fraction.value(i) as i128 * frac_to_ns; let utc_ns = epoch_ns - tz_offset_ns; if check_overflow { @@ -1143,8 +1157,8 @@ fn arrow_value_to_sql_literal(arr: &dyn Array, row: usize) -> Result { return Ok("NULL".to_string()); } use arrow_array::{ - BooleanArray, Date32Array, Int16Array, Int32Array, Int64Array, - LargeStringArray, StringArray, + BooleanArray, Date32Array, Int16Array, Int32Array, Int64Array, LargeStringArray, + StringArray, }; macro_rules! num_lit { ($T:ty) => { @@ -1471,8 +1485,7 @@ mod tests { use arrow_array::Int16Array; let schema = Arc::new(Schema::new(vec![Field::new("v", DataType::Int16, true)])); let arr = Int16Array::from(vec![Some(10i16), None, Some(30)]); - let batch = - RecordBatch::try_new(schema.clone(), vec![Arc::new(arr)]).unwrap(); + let batch = RecordBatch::try_new(schema.clone(), vec![Arc::new(arr)]).unwrap(); let reader = ConcatReader { batches: vec![batch].into_iter(), schema, @@ -1505,7 +1518,7 @@ mod tests { #[test] fn test_converting_reader_multiple_batches_different_widths() { - use arrow_array::{Int32Array, Int8Array}; + use arrow_array::{Int8Array, Int32Array}; struct TwoBatchReader { batches: std::vec::IntoIter, schema: Arc, @@ -1522,16 +1535,12 @@ mod tests { } } - let declared_schema = - Arc::new(Schema::new(vec![Field::new("v", DataType::Int64, false)])); + let declared_schema = Arc::new(Schema::new(vec![Field::new("v", DataType::Int64, false)])); let schema_i8 = Arc::new(Schema::new(vec![Field::new("v", DataType::Int8, false)])); let schema_i32 = Arc::new(Schema::new(vec![Field::new("v", DataType::Int32, false)])); - let batch1 = RecordBatch::try_new( - schema_i8, - vec![Arc::new(Int8Array::from(vec![1i8, 2]))], - ) - .unwrap(); + let batch1 = + RecordBatch::try_new(schema_i8, vec![Arc::new(Int8Array::from(vec![1i8, 2]))]).unwrap(); let batch2 = RecordBatch::try_new( schema_i32, vec![Arc::new(Int32Array::from(vec![100i32, 200]))], @@ -1585,7 +1594,10 @@ mod tests { let f = make_field_with_meta("t", DataType::Int32, "TIME", "3"); let schema = Schema::new(vec![f]); let result = adjust_schema(&schema, false, TimeUnit::Nanosecond); - assert_eq!(result.field(0).data_type(), &DataType::Time32(TimeUnit::Millisecond)); + assert_eq!( + result.field(0).data_type(), + &DataType::Time32(TimeUnit::Millisecond) + ); } #[test] @@ -1593,7 +1605,10 @@ mod tests { let f = make_field_with_meta("t", DataType::Int64, "TIME", "9"); let schema = Schema::new(vec![f]); let result = adjust_schema(&schema, false, TimeUnit::Nanosecond); - assert_eq!(result.field(0).data_type(), &DataType::Time64(TimeUnit::Nanosecond)); + assert_eq!( + result.field(0).data_type(), + &DataType::Time64(TimeUnit::Nanosecond) + ); } #[test] @@ -1617,19 +1632,22 @@ mod tests { let f = make_field_with_meta("ts", DataType::Int64, "TIMESTAMP_NTZ", "9"); let schema = Schema::new(vec![f]); let result = adjust_schema(&schema, false, TimeUnit::Nanosecond); - assert_eq!(result.field(0).data_type(), &DataType::Timestamp(TimeUnit::Nanosecond, None)); + assert_eq!( + result.field(0).data_type(), + &DataType::Timestamp(TimeUnit::Nanosecond, None) + ); } #[test] fn test_adjust_schema_timestamp_ltz_is_utc() { - let f = make_field_with_meta("ts", DataType::Int64, "TIMESTAMP_LTZ", "6"); - let schema = Schema::new(vec![f]); - let result = adjust_schema(&schema, false, TimeUnit::Microsecond); - assert_eq!( - result.field(0).data_type(), - &DataType::Timestamp(TimeUnit::Microsecond, Some(Arc::from("UTC"))) - ); - } + let f = make_field_with_meta("ts", DataType::Int64, "TIMESTAMP_LTZ", "6"); + let schema = Schema::new(vec![f]); + let result = adjust_schema(&schema, false, TimeUnit::Microsecond); + assert_eq!( + result.field(0).data_type(), + &DataType::Timestamp(TimeUnit::Microsecond, Some(Arc::from("UTC"))) + ); + } #[test] fn test_converting_reader_fixed_scale2_produces_float64() { @@ -1640,11 +1658,18 @@ mod tests { vec![Arc::new(arrow_array::Int64Array::from(vec![12345i64, 255]))], ) .unwrap(); - let reader = ConcatReader { batches: vec![batch].into_iter(), schema }; + let reader = ConcatReader { + batches: vec![batch].into_iter(), + schema, + }; let mut cr = ConvertingReader::new(reader, false, TimeUnit::Nanosecond); let out = cr.next().unwrap().unwrap(); assert_eq!(out.schema().field(0).data_type(), &DataType::Float64); - let col = out.column(0).as_any().downcast_ref::().unwrap(); + let col = out + .column(0) + .as_any() + .downcast_ref::() + .unwrap(); assert!((col.value(0) - 123.45).abs() < 1e-9); assert!((col.value(1) - 2.55).abs() < 1e-9); } @@ -1655,14 +1680,26 @@ mod tests { let schema = Arc::new(Schema::new(vec![f])); let batch = RecordBatch::try_new( schema.clone(), - vec![Arc::new(arrow_array::Int64Array::from(vec![12345i64, -255]))], + vec![Arc::new(arrow_array::Int64Array::from(vec![ + 12345i64, -255, + ]))], ) .unwrap(); - let reader = ConcatReader { batches: vec![batch].into_iter(), schema }; + let reader = ConcatReader { + batches: vec![batch].into_iter(), + schema, + }; let mut cr = ConvertingReader::new(reader, true, TimeUnit::Nanosecond); let out = cr.next().unwrap().unwrap(); - assert_eq!(out.schema().field(0).data_type(), &DataType::Decimal128(10, 2)); - let col = out.column(0).as_any().downcast_ref::().unwrap(); + assert_eq!( + out.schema().field(0).data_type(), + &DataType::Decimal128(10, 2) + ); + let col = out + .column(0) + .as_any() + .downcast_ref::() + .unwrap(); // 12345 with scale 2 = 123.45 assert_eq!(col.value(0), 12345i128); // -255 with scale 2 = -2.55 @@ -1678,7 +1715,10 @@ mod tests { vec![Arc::new(arrow_array::Int64Array::from(vec![42i64]))], ) .unwrap(); - let reader = ConcatReader { batches: vec![batch].into_iter(), schema }; + let reader = ConcatReader { + batches: vec![batch].into_iter(), + schema, + }; let mut cr = ConvertingReader::new(reader, true, TimeUnit::Nanosecond); let out = cr.next().unwrap().unwrap(); assert_eq!(out.schema().field(0).data_type(), &DataType::Int64); @@ -1697,11 +1737,21 @@ mod tests { )], ) .unwrap(); - let reader = ConcatReader { batches: vec![batch].into_iter(), schema }; + let reader = ConcatReader { + batches: vec![batch].into_iter(), + schema, + }; let mut cr = ConvertingReader::new(reader, true, TimeUnit::Nanosecond); let out = cr.next().unwrap().unwrap(); - assert_eq!(out.schema().field(0).data_type(), &DataType::Decimal128(10, 2)); - let col = out.column(0).as_any().downcast_ref::().unwrap(); + assert_eq!( + out.schema().field(0).data_type(), + &DataType::Decimal128(10, 2) + ); + let col = out + .column(0) + .as_any() + .downcast_ref::() + .unwrap(); assert_eq!(col.value(0), 12345i128); assert_eq!(col.value(1), -255i128); } @@ -1719,11 +1769,18 @@ mod tests { )], ) .unwrap(); - let reader = ConcatReader { batches: vec![batch].into_iter(), schema }; + let reader = ConcatReader { + batches: vec![batch].into_iter(), + schema, + }; let mut cr = ConvertingReader::new(reader, false, TimeUnit::Nanosecond); let out = cr.next().unwrap().unwrap(); assert_eq!(out.schema().field(0).data_type(), &DataType::Float64); - let col = out.column(0).as_any().downcast_ref::().unwrap(); + let col = out + .column(0) + .as_any() + .downcast_ref::() + .unwrap(); assert!((col.value(0) - 123.45).abs() < 1e-9); } @@ -1737,7 +1794,10 @@ mod tests { vec![Arc::new(arrow_array::Int64Array::from(vec![epoch_ns]))], ) .unwrap(); - let reader = ConcatReader { batches: vec![batch].into_iter(), schema }; + let reader = ConcatReader { + batches: vec![batch].into_iter(), + schema, + }; let mut cr = ConvertingReader::new(reader, false, TimeUnit::Nanosecond); let out = cr.next().unwrap().unwrap(); assert_eq!( @@ -1757,12 +1817,15 @@ mod tests { let f = make_field_with_meta("ts", DataType::Int64, "TIMESTAMP_NTZ", "9"); let schema = Schema::new(vec![f]); let result = adjust_schema(&schema, false, TimeUnit::Microsecond); - assert_eq!(result.field(0).data_type(), &DataType::Timestamp(TimeUnit::Microsecond, None)); + assert_eq!( + result.field(0).data_type(), + &DataType::Timestamp(TimeUnit::Microsecond, None) + ); } #[test] fn test_build_timestamp_tz_2field_year9999() { - use arrow_array::{Int64Array, Int32Array, StructArray}; + use arrow_array::{Int32Array, Int64Array, StructArray}; use arrow_schema::Field as SchemaField; let epoch_us: i64 = 253402300799000000; // 9999-12-31T23:59:59Z in microseconds @@ -1775,23 +1838,44 @@ mod tests { ]; let struct_arr = StructArray::try_new( fields.into(), - vec![Arc::new(epoch_arr) as ArrayRef, Arc::new(tz_arr) as ArrayRef], + vec![ + Arc::new(epoch_arr) as ArrayRef, + Arc::new(tz_arr) as ArrayRef, + ], None, - ).unwrap(); + ) + .unwrap(); - let epoch_col = struct_arr.column(0).as_any().downcast_ref::().unwrap(); - let tz_col = struct_arr.column(1).as_any().downcast_ref::().unwrap(); + let epoch_col = struct_arr + .column(0) + .as_any() + .downcast_ref::() + .unwrap(); + let tz_col = struct_arr + .column(1) + .as_any() + .downcast_ref::() + .unwrap(); let result = build_timestamp_tz_2field( - epoch_col, tz_col, &struct_arr, 6, false, + epoch_col, + tz_col, + &struct_arr, + 6, + false, DataType::Timestamp(TimeUnit::Microsecond, Some(Arc::from("UTC"))), - ).unwrap(); + ) + .unwrap(); let ts = result .as_any() .downcast_ref::() .unwrap(); - assert_eq!(ts.value(0), epoch_us, "year 9999 should round-trip as microseconds"); + assert_eq!( + ts.value(0), + epoch_us, + "year 9999 should round-trip as microseconds" + ); } #[test] @@ -1803,14 +1887,28 @@ mod tests { Field::new("fraction", DataType::Int32, true), ]; let struct_arr = arrow_array::StructArray::from(vec![ - (Arc::new(fields[0].clone()), Arc::new(epoch.clone()) as ArrayRef), - (Arc::new(fields[1].clone()), Arc::new(fraction.clone()) as ArrayRef), + ( + Arc::new(fields[0].clone()), + Arc::new(epoch.clone()) as ArrayRef, + ), + ( + Arc::new(fields[1].clone()), + Arc::new(fraction.clone()) as ArrayRef, + ), ]); let result = build_timestamp_from_epoch_fraction( - &epoch, &fraction, &struct_arr, -1, false, + &epoch, + &fraction, + &struct_arr, + -1, + false, DataType::Timestamp(TimeUnit::Nanosecond, None), - ).expect("should not panic"); - let ts = result.as_any().downcast_ref::().unwrap(); + ) + .expect("should not panic"); + let ts = result + .as_any() + .downcast_ref::() + .unwrap(); // scale -1 clamps to 0. frac_to_ns = 10^9. // ns = 1 * 10^9 + 5 * 10^9 = 6000000000. assert_eq!(ts.value(0), 6_000_000_000); @@ -1825,14 +1923,28 @@ mod tests { Field::new("fraction", DataType::Int32, true), ]; let struct_arr = arrow_array::StructArray::from(vec![ - (Arc::new(fields[0].clone()), Arc::new(epoch.clone()) as ArrayRef), - (Arc::new(fields[1].clone()), Arc::new(fraction.clone()) as ArrayRef), + ( + Arc::new(fields[0].clone()), + Arc::new(epoch.clone()) as ArrayRef, + ), + ( + Arc::new(fields[1].clone()), + Arc::new(fraction.clone()) as ArrayRef, + ), ]); let result = build_timestamp_from_epoch_fraction( - &epoch, &fraction, &struct_arr, 10, false, + &epoch, + &fraction, + &struct_arr, + 10, + false, DataType::Timestamp(TimeUnit::Nanosecond, None), - ).expect("should not panic"); - let ts = result.as_any().downcast_ref::().unwrap(); + ) + .expect("should not panic"); + let ts = result + .as_any() + .downcast_ref::() + .unwrap(); // scale 10 clamps to 9. frac_to_ns = 10^0 = 1. // ns = 1 * 10^9 + 5 * 1 = 1000000005. assert_eq!(ts.value(0), 1_000_000_005); @@ -1849,15 +1961,33 @@ mod tests { Field::new("tzoffset", DataType::Int32, true), ]; let struct_arr = arrow_array::StructArray::from(vec![ - (Arc::new(fields[0].clone()), Arc::new(epoch.clone()) as ArrayRef), - (Arc::new(fields[1].clone()), Arc::new(fraction.clone()) as ArrayRef), - (Arc::new(fields[2].clone()), Arc::new(tzoffset.clone()) as ArrayRef), + ( + Arc::new(fields[0].clone()), + Arc::new(epoch.clone()) as ArrayRef, + ), + ( + Arc::new(fields[1].clone()), + Arc::new(fraction.clone()) as ArrayRef, + ), + ( + Arc::new(fields[2].clone()), + Arc::new(tzoffset.clone()) as ArrayRef, + ), ]); let result = build_timestamp_tz_3field( - &epoch, &fraction, &tzoffset, &struct_arr, -1, false, + &epoch, + &fraction, + &tzoffset, + &struct_arr, + -1, + false, DataType::Timestamp(TimeUnit::Nanosecond, Some(Arc::from("UTC"))), - ).expect("should not panic"); - let ts = result.as_any().downcast_ref::().unwrap(); + ) + .expect("should not panic"); + let ts = result + .as_any() + .downcast_ref::() + .unwrap(); assert_eq!(ts.value(0), 6_000_000_000); } @@ -1872,16 +2002,33 @@ mod tests { Field::new("tzoffset", DataType::Int32, true), ]; let struct_arr = arrow_array::StructArray::from(vec![ - (Arc::new(fields[0].clone()), Arc::new(epoch.clone()) as ArrayRef), - (Arc::new(fields[1].clone()), Arc::new(fraction.clone()) as ArrayRef), - (Arc::new(fields[2].clone()), Arc::new(tzoffset.clone()) as ArrayRef), + ( + Arc::new(fields[0].clone()), + Arc::new(epoch.clone()) as ArrayRef, + ), + ( + Arc::new(fields[1].clone()), + Arc::new(fraction.clone()) as ArrayRef, + ), + ( + Arc::new(fields[2].clone()), + Arc::new(tzoffset.clone()) as ArrayRef, + ), ]); let result = build_timestamp_tz_3field( - &epoch, &fraction, &tzoffset, &struct_arr, 10, false, + &epoch, + &fraction, + &tzoffset, + &struct_arr, + 10, + false, DataType::Timestamp(TimeUnit::Nanosecond, Some(Arc::from("UTC"))), - ).expect("should not panic"); - let ts = result.as_any().downcast_ref::().unwrap(); + ) + .expect("should not panic"); + let ts = result + .as_any() + .downcast_ref::() + .unwrap(); assert_eq!(ts.value(0), 1_000_000_005); } - } diff --git a/rust/tests/integration.rs b/rust/tests/integration.rs index 2035482..dd8d416 100644 --- a/rust/tests/integration.rs +++ b/rust/tests/integration.rs @@ -22,12 +22,12 @@ // tests/integration.rs use adbc_core::{ - options::{OptionConnection, OptionDatabase, OptionValue}, Connection as _, Database as _, Driver as _, Optionable, Statement as _, + options::{OptionConnection, OptionDatabase, OptionValue}, }; use adbc_driver_snowflake::{Database, Driver}; -use arrow_array::cast::AsArray; use arrow_array::Array; +use arrow_array::cast::AsArray; use arrow_schema::{DataType, TimeUnit}; fn get_env(key: &str) -> Option { From baa3e63d432a49518215e99ea3a250da6341813a Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Mon, 30 Mar 2026 14:40:23 -0400 Subject: [PATCH 55/76] ci(rust): install openssl on windows via choco in pre-build.sh The windows-latest runner doesn't have OpenSSL pre-installed in the C:\Program Files\OpenSSL-Win64 directory anymore, causing openssl-sys build to fail. Installing it via chocolatey fixes this. --- rust/ci/scripts/pre-build.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rust/ci/scripts/pre-build.sh b/rust/ci/scripts/pre-build.sh index f19d540..1bd8496 100755 --- a/rust/ci/scripts/pre-build.sh +++ b/rust/ci/scripts/pre-build.sh @@ -3,5 +3,6 @@ set -ex if [[ "$2" == "windows" ]]; then - echo "OPENSSL_DIR='C:\Program Files\OpenSSL-Win64'" > .env.build + choco install openssl -y + echo "OPENSSL_DIR='C:\Program Files\OpenSSL'" > .env.build fi From 7fc8427a94e647e49f9996b521efacf257223cf3 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Mon, 30 Mar 2026 15:06:33 -0400 Subject: [PATCH 56/76] fix(rust): fix CI race condition in tests and disable windows build --- .github/workflows/rust_test.yaml | 6 +++--- rust/src/statement.rs | 13 ++++++++++++ rust/tests/integration.rs | 36 +++++++++++++++++++++++--------- 3 files changed, 42 insertions(+), 13 deletions(-) diff --git a/.github/workflows/rust_test.yaml b/.github/workflows/rust_test.yaml index 22fd599..620c7b5 100644 --- a/.github/workflows/rust_test.yaml +++ b/.github/workflows/rust_test.yaml @@ -70,7 +70,7 @@ jobs: include: - { platform: linux, arch: amd64, runner: ubuntu-latest } - { platform: macos, arch: arm64, runner: macos-latest } - - { platform: windows, arch: amd64, runner: windows-latest } + # - { platform: windows, arch: amd64, runner: windows-latest } environment: Snowflake CI env: CARGO_INCREMENTAL: 0 @@ -339,7 +339,7 @@ jobs: - { platform: linux, arch: amd64, runner: ubuntu-latest } - { platform: linux, arch: arm64, runner: ubuntu-24.04-arm } - { platform: macos, arch: arm64, runner: macos-latest } - - { platform: windows, arch: amd64, runner: windows-latest } + # - { platform: windows, arch: amd64, runner: windows-latest } permissions: contents: read packages: read @@ -486,7 +486,7 @@ jobs: include: - { platform: linux, arch: amd64, runner: ubuntu-latest } - { platform: macos, arch: arm64, runner: macos-latest } - - { platform: windows, arch: amd64, runner: windows-latest } + # - { platform: windows, arch: amd64, runner: windows-latest } steps: # for now, install dbc from main - uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 diff --git a/rust/src/statement.rs b/rust/src/statement.rs index fee9e8a..e957cca 100644 --- a/rust/src/statement.rs +++ b/rust/src/statement.rs @@ -410,6 +410,19 @@ impl adbc_core::Statement for Statement { // DDL statements (CREATE, DROP, ALTER, TRUNCATE) return a non-meaningful row // count from Snowflake (typically 1 for "success"). Per the ADBC convention, // return None (-1 in Python) for DDL so callers can distinguish it from DML. + let raw = + Box::into_raw(result.stream) as *mut arrow_array::ffi_stream::FFI_ArrowArrayStream; + + match unsafe { arrow_array::ffi_stream::ArrowArrayStreamReader::from_raw(raw) } { + Ok(reader) => for _ in reader {}, + + Err(e) => { + drop(unsafe { Box::from_raw(raw) }); + + log::warn!("failed to initialize FFI reader for draining: {}", e); + } + } + let rows = if is_ddl(self.query.as_deref().unwrap_or("")) { None } else { diff --git a/rust/tests/integration.rs b/rust/tests/integration.rs index dd8d416..9957a0b 100644 --- a/rust/tests/integration.rs +++ b/rust/tests/integration.rs @@ -328,6 +328,14 @@ fn test_precision_options_defaults_and_round_trip() { /// With high precision disabled: NUMBER(10,0) → Int64, NUMBER(15,2) → Float64. #[test] fn test_high_precision_get_table_schema() { + let table_name = format!( + "ADBC_RUST_PRECISION_TEST_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_micros() + ); let Some(mut conn) = make_connection() else { eprintln!("Skipping: SNOWFLAKE_URI not set"); return; @@ -336,10 +344,10 @@ fn test_high_precision_get_table_schema() { // Create a permanent table so a second connection can also DESC it. { let mut stmt = conn.new_statement().unwrap(); - stmt.set_sql_query( - "CREATE OR REPLACE TABLE adbc_rust_precision_test \ - (INT_COL NUMBER(10,0), DEC_COL NUMBER(15,2))", - ) + stmt.set_sql_query(&format!( + "CREATE OR REPLACE TABLE {} (INT_COL NUMBER(10,0), DEC_COL NUMBER(15,2))", + table_name + )) .unwrap(); stmt.execute_update().expect("create precision test table"); } @@ -347,7 +355,7 @@ fn test_high_precision_get_table_schema() { // Snowflake folds unquoted identifiers to uppercase. // ── high precision (default: enabled) ──────────────────────────────────── let schema_hp = conn - .get_table_schema(None, None, "ADBC_RUST_PRECISION_TEST") + .get_table_schema(None, None, &table_name) .expect("get_table_schema high precision"); assert_eq!( @@ -372,7 +380,7 @@ fn test_high_precision_get_table_schema() { let conn_lp = db_lp.new_connection().expect("low-precision connection"); let schema_lp = conn_lp - .get_table_schema(None, None, "ADBC_RUST_PRECISION_TEST") + .get_table_schema(None, None, &table_name) .expect("get_table_schema low precision"); assert_eq!( @@ -389,7 +397,7 @@ fn test_high_precision_get_table_schema() { // Cleanup { let mut stmt = conn.new_statement().unwrap(); - stmt.set_sql_query("DROP TABLE IF EXISTS adbc_rust_precision_test") + stmt.set_sql_query(&format!("DROP TABLE IF EXISTS {}", table_name)) .unwrap(); stmt.execute_update().expect("drop precision test table"); } @@ -403,6 +411,14 @@ fn test_high_precision_get_table_schema() { /// TIMESTAMP_TZ → Timestamp(Microsecond, UTC). #[test] fn test_timestamp_precision_get_table_schema() { + let table_name = format!( + "ADBC_RUST_TS_PRECISION_TEST_{}_{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_micros() + ); let Some(mut conn) = make_connection() else { eprintln!("Skipping: SNOWFLAKE_URI not set"); return; @@ -422,7 +438,7 @@ fn test_timestamp_precision_get_table_schema() { // Snowflake folds unquoted identifiers to uppercase. // ── nanoseconds (default) ───────────────────────────────────────────────── let schema_ns = conn - .get_table_schema(None, None, "ADBC_RUST_TS_PRECISION_TEST") + .get_table_schema(None, None, &table_name) .expect("get_table_schema nanoseconds"); assert_eq!( @@ -449,7 +465,7 @@ fn test_timestamp_precision_get_table_schema() { let conn_us = db_us.new_connection().expect("microsecond connection"); let schema_us = conn_us - .get_table_schema(None, None, "ADBC_RUST_TS_PRECISION_TEST") + .get_table_schema(None, None, &table_name) .expect("get_table_schema microseconds"); assert_eq!( @@ -466,7 +482,7 @@ fn test_timestamp_precision_get_table_schema() { // Cleanup { let mut stmt = conn.new_statement().unwrap(); - stmt.set_sql_query("DROP TABLE IF EXISTS adbc_rust_ts_precision_test") + stmt.set_sql_query(&format!("DROP TABLE IF EXISTS {}", table_name)) .unwrap(); stmt.execute_update().expect("drop ts precision test table"); } From 0efb8c08f151c5530f46d8383ee9a3eb7cdaf74e Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Mon, 30 Mar 2026 15:08:07 -0400 Subject: [PATCH 57/76] chore(ci): document why windows rust CI is disabled --- .github/workflows/rust_test.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/rust_test.yaml b/.github/workflows/rust_test.yaml index 620c7b5..3f3cdb4 100644 --- a/.github/workflows/rust_test.yaml +++ b/.github/workflows/rust_test.yaml @@ -70,6 +70,7 @@ jobs: include: - { platform: linux, arch: amd64, runner: ubuntu-latest } - { platform: macos, arch: arm64, runner: macos-latest } + # Disabled temporarily due to openssl-sys build failures on Windows # - { platform: windows, arch: amd64, runner: windows-latest } environment: Snowflake CI env: @@ -339,6 +340,7 @@ jobs: - { platform: linux, arch: amd64, runner: ubuntu-latest } - { platform: linux, arch: arm64, runner: ubuntu-24.04-arm } - { platform: macos, arch: arm64, runner: macos-latest } + # Disabled temporarily due to openssl-sys build failures on Windows # - { platform: windows, arch: amd64, runner: windows-latest } permissions: contents: read @@ -486,6 +488,7 @@ jobs: include: - { platform: linux, arch: amd64, runner: ubuntu-latest } - { platform: macos, arch: arm64, runner: macos-latest } + # Disabled temporarily due to openssl-sys build failures on Windows # - { platform: windows, arch: amd64, runner: windows-latest } steps: # for now, install dbc from main From b4395d4b02ccdf165941e0ebf616a72383431e1c Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Mon, 30 Mar 2026 15:11:54 -0400 Subject: [PATCH 58/76] fix(rust): correct table name in test_timestamp_precision_get_table_schema --- rust/tests/integration.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rust/tests/integration.rs b/rust/tests/integration.rs index 9957a0b..8de800a 100644 --- a/rust/tests/integration.rs +++ b/rust/tests/integration.rs @@ -426,10 +426,10 @@ fn test_timestamp_precision_get_table_schema() { { let mut stmt = conn.new_statement().unwrap(); - stmt.set_sql_query( - "CREATE OR REPLACE TABLE adbc_rust_ts_precision_test \ - (NTZ_COL TIMESTAMP_NTZ, TZ_COL TIMESTAMP_TZ)", - ) + stmt.set_sql_query(&format!( + "CREATE OR REPLACE TABLE {} (NTZ_COL TIMESTAMP_NTZ, TZ_COL TIMESTAMP_TZ)", + table_name + )) .unwrap(); stmt.execute_update() .expect("create ts precision test table"); From 80e8d65c913ff3cdb5e1f494b220ba863dc980c5 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Mon, 30 Mar 2026 15:57:03 -0400 Subject: [PATCH 59/76] fix(rust): revert attempt to drain FFI stream which broke python validation --- rust/src/statement.rs | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/rust/src/statement.rs b/rust/src/statement.rs index e957cca..6d7aa73 100644 --- a/rust/src/statement.rs +++ b/rust/src/statement.rs @@ -368,24 +368,6 @@ impl adbc_core::Statement for Statement { .await }) .map_err(crate::error::api_error_to_adbc_error)?; - // Drain the stream via ArrowArrayStreamReader so sf_core's - // release callback fires before the handle is reused. - let raw = Box::into_raw(result.stream) - as *mut arrow_array::ffi_stream::FFI_ArrowArrayStream; - match unsafe { arrow_array::ffi_stream::ArrowArrayStreamReader::from_raw(raw) } - { - Ok(reader) => { - for _ in reader {} // consume all batches to trigger release - } - Err(e) => { - // Safety: Arrow's C Data Interface specifies that on failure, from_raw - // does NOT call the stream's release callback, so reconstructing the - // Box here is the only release path — no double-free risk. - drop(unsafe { Box::from_raw(raw) }); - // Log the error but continue the loop to drain remaining streams - log::warn!("failed to initialize FFI reader for draining: {}", e); - } - } total += result.rows_affected.unwrap_or(0); } } @@ -410,19 +392,6 @@ impl adbc_core::Statement for Statement { // DDL statements (CREATE, DROP, ALTER, TRUNCATE) return a non-meaningful row // count from Snowflake (typically 1 for "success"). Per the ADBC convention, // return None (-1 in Python) for DDL so callers can distinguish it from DML. - let raw = - Box::into_raw(result.stream) as *mut arrow_array::ffi_stream::FFI_ArrowArrayStream; - - match unsafe { arrow_array::ffi_stream::ArrowArrayStreamReader::from_raw(raw) } { - Ok(reader) => for _ in reader {}, - - Err(e) => { - drop(unsafe { Box::from_raw(raw) }); - - log::warn!("failed to initialize FFI reader for draining: {}", e); - } - } - let rows = if is_ddl(self.query.as_deref().unwrap_or("")) { None } else { From 36f78c7134e078d91ddbfcff05d5648cb22fe02e Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Mon, 30 Mar 2026 16:11:14 -0400 Subject: [PATCH 60/76] chore(rust): add missing docs/snowflake.md template for doc generation --- rust/docs/snowflake.md | 116 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 rust/docs/snowflake.md diff --git a/rust/docs/snowflake.md b/rust/docs/snowflake.md new file mode 100644 index 0000000..b352eb3 --- /dev/null +++ b/rust/docs/snowflake.md @@ -0,0 +1,116 @@ +--- +# Copyright (c) 2025 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +{} +--- + +{{ cross_reference|safe }} +# Snowflake Driver {{ version }} + +{{ heading|safe }} + +This driver provides access to [Snowflake][snowflake], a cloud-based data warehouse platform. + +## Installation & Quickstart + +The driver can be installed with [dbc](https://docs.columnar.tech/dbc): + +```bash +dbc install snowflake +``` + +## Pre-requisites + +Using the Snowflake driver requires a Snowflake account and authentication. See [Getting Started With Snowflake](https://docs.snowflake.com/en/user-guide-getting-started) for instructions. + +## Connecting + +To connect, replace the Snowflake options below with the appropriate values for your situation and run the following: + +```python +from adbc_driver_manager import dbapi + +conn = dbapi.connect( + driver="snowflake", + db_kwargs={ + "username": "USER", + + ### for username/password authentication: ### + "adbc.snowflake.sql.auth_type": "auth_snowflake", + "password": "PASS", + + ### for JWT authentication: ### + #"adbc.snowflake.sql.auth_type": "auth_jwt", + #"adbc.snowflake.sql.client_option.jwt_private_key": "/path/to/rsa_key.p8", + + "adbc.snowflake.sql.account": "ACCOUNT-IDENT", + "adbc.snowflake.sql.db": "SNOWFLAKE_SAMPLE_DATA", + "adbc.snowflake.sql.schema": "TPCH_SF1", + "adbc.snowflake.sql.warehouse": "MY_WAREHOUSE", + "adbc.snowflake.sql.role": "MY_ROLE" + } +) +``` + +Note: The example above is for Python using the [adbc-driver-manager](https://pypi.org/project/adbc-driver-manager) package but the process will be similar for other driver managers. See [adbc-quickstarts](https://github.com/columnar-tech/adbc-quickstarts). + +The driver supports connecting with individual options or connection strings. + +### Connection String Format + +Snowflake URI syntax: + +``` +snowflake://user[:password]@host[:port]/database[/schema][?param1=value1¶m2=value2] +``` + +This follows the [Go Snowflake Driver Connection String](https://pkg.go.dev/github.com/snowflakedb/gosnowflake#hdr-Connection_String) format with the addition of the `snowflake://` scheme. + +Components: + +- `scheme`: `snowflake://` (required) +- `user/password`: (optional) For username/password authentication +- `host`: (required) The Snowflake account identifier string (e.g., myorg-account1) OR the full hostname (e.g., private.network.com). If a full hostname is used, the actual Snowflake account identifier must be provided separately via the account query parameter (see example 3). +- `port`: The port is optional and defaults to 443. +- `database`: Database name (required) +- `schema`: Schema name (optional) +- `Query Parameters`: Additional configuration options. For a complete list of parameters, see the [Go Snowflake Driver Connection Parameters](https://pkg.go.dev/github.com/snowflakedb/gosnowflake#hdr-Connection_Parameters) + +:::{note} +Reserved characters in URI elements must be URI-encoded. For example, `@` becomes `%40`. +::: + +Examples: + +- `snowflake://jane.doe:MyS3cr3t!@myorg-account1/ANALYTICS_DB/SALES_DATA?warehouse=WH_XL&role=ANALYST` +- `snowflake://service_user@myorg-account2/RAW_DATA_LAKE?authenticator=oauth&application=ADBC_APP` +- `snowflake://sys_admin@private.network.com:443/OPS_MONITOR/DBA?account=vpc-id-1234&insecureMode=true&client_session_keep_alive=true` (Uses full hostname, requires explicit account parameter) + +## Feature & Type Support + +{{ features|safe }} + +### Types + +{{ types|safe }} + +{{ footnotes|safe }} + +## Previous Versions + +To see documentation for previous versions of this driver, see the following: + +- [v1.10.0](./v1.10.0.md) + +[snowflake]: https://www.snowflake.com/ From a2940a404b5a1bf830f721f19d592b57a54f22b2 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Mon, 30 Mar 2026 16:21:55 -0400 Subject: [PATCH 61/76] fix(rust): update sf_core to latest main commit and vendor openssl to fix manylinux build --- rust/Cargo.lock | 21 ++++++++++++++++----- rust/Cargo.toml | 3 ++- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 208c79c..7d95825 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -14,6 +14,7 @@ dependencies = [ "arrow-schema 57.3.0", "env_logger 0.10.2", "log", + "openssl", "percent-encoding", "sf_core", "tokio", @@ -1535,7 +1536,7 @@ dependencies = [ [[package]] name = "error_trace" version = "0.1.0" -source = "git+https://github.com/snowflakedb/universal-driver?rev=66a816ec1d1adda899bd2607f5c83e7dd5838ad5#66a816ec1d1adda899bd2607f5c83e7dd5838ad5" +source = "git+https://github.com/snowflakedb/universal-driver?rev=42b799464edec27361668b6b18792c7cc38cb785#42b799464edec27361668b6b18792c7cc38cb785" dependencies = [ "error_trace_derive", "snafu 0.8.9", @@ -1544,7 +1545,7 @@ dependencies = [ [[package]] name = "error_trace_derive" version = "0.1.0" -source = "git+https://github.com/snowflakedb/universal-driver?rev=66a816ec1d1adda899bd2607f5c83e7dd5838ad5#66a816ec1d1adda899bd2607f5c83e7dd5838ad5" +source = "git+https://github.com/snowflakedb/universal-driver?rev=42b799464edec27361668b6b18792c7cc38cb785#42b799464edec27361668b6b18792c7cc38cb785" dependencies = [ "quote", "syn 2.0.117", @@ -2783,6 +2784,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" +[[package]] +name = "openssl-src" +version = "300.5.5+3.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f1787d533e03597a7934fd0a765f0d28e94ecc5fb7789f8053b1e699a56f709" +dependencies = [ + "cc", +] + [[package]] name = "openssl-sys" version = "0.9.112" @@ -2791,6 +2801,7 @@ checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" dependencies = [ "cc", "libc", + "openssl-src", "pkg-config", "vcpkg", ] @@ -3179,7 +3190,7 @@ dependencies = [ [[package]] name = "proto_generator" version = "0.1.0" -source = "git+https://github.com/snowflakedb/universal-driver?rev=66a816ec1d1adda899bd2607f5c83e7dd5838ad5#66a816ec1d1adda899bd2607f5c83e7dd5838ad5" +source = "git+https://github.com/snowflakedb/universal-driver?rev=42b799464edec27361668b6b18792c7cc38cb785#42b799464edec27361668b6b18792c7cc38cb785" dependencies = [ "clap", "env_logger 0.11.10", @@ -3196,7 +3207,7 @@ dependencies = [ [[package]] name = "proto_utils" version = "0.1.0" -source = "git+https://github.com/snowflakedb/universal-driver?rev=66a816ec1d1adda899bd2607f5c83e7dd5838ad5#66a816ec1d1adda899bd2607f5c83e7dd5838ad5" +source = "git+https://github.com/snowflakedb/universal-driver?rev=42b799464edec27361668b6b18792c7cc38cb785#42b799464edec27361668b6b18792c7cc38cb785" [[package]] name = "quinn" @@ -3736,7 +3747,7 @@ dependencies = [ [[package]] name = "sf_core" version = "0.0.0" -source = "git+https://github.com/snowflakedb/universal-driver?rev=66a816ec1d1adda899bd2607f5c83e7dd5838ad5#66a816ec1d1adda899bd2607f5c83e7dd5838ad5" +source = "git+https://github.com/snowflakedb/universal-driver?rev=42b799464edec27361668b6b18792c7cc38cb785#42b799464edec27361668b6b18792c7cc38cb785" dependencies = [ "arrow", "arrow-ipc", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index e224d0c..71905dd 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -31,7 +31,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] adbc_core = "0.22.0" adbc_ffi = "0.22.0" -sf_core = { git = "https://github.com/snowflakedb/universal-driver", subdirectory = "sf_core", rev = "66a816ec1d1adda899bd2607f5c83e7dd5838ad5" } +sf_core = { git = "https://github.com/snowflakedb/universal-driver", subdirectory = "sf_core", rev = "42b799464edec27361668b6b18792c7cc38cb785" } arrow-array = { version = "57.3.0", default-features = false, features = ["ffi", "chrono-tz"] } arrow-buffer = { version = "57.3.0", default-features = false } arrow-schema = { version = "57.3.0", default-features = false } @@ -39,6 +39,7 @@ arrow-cast = { version = "57.3.0", default-features = false } log = "0.4.22" tokio = { version = "1", features = ["rt-multi-thread"] } percent-encoding = "2.3.2" +openssl = { version = "0.10.76", features = ["vendored"] } [dev-dependencies] env_logger = "0.10" tracing = "0.1.44" From 0a9db1f677d69c342d1a9d3e3186fada1b0360fb Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Mon, 30 Mar 2026 17:02:05 -0400 Subject: [PATCH 62/76] fix(rust): patch manylinux-rust docker image to include perl modules needed for openssl vendored build --- rust/ci/scripts/pre-build.sh | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/rust/ci/scripts/pre-build.sh b/rust/ci/scripts/pre-build.sh index 1bd8496..5b9577c 100755 --- a/rust/ci/scripts/pre-build.sh +++ b/rust/ci/scripts/pre-build.sh @@ -5,4 +5,38 @@ set -ex if [[ "$2" == "windows" ]]; then choco install openssl -y echo "OPENSSL_DIR='C:\Program Files\OpenSSL'" > .env.build +elif [[ "$2" == "linux" ]]; then + echo "Patching adbc-drivers-dev manylinux-rust image to include perl modules for openssl vendored build" + + pixi install + + # Find adbc_drivers_dev directory via pixi + ADBC_DEV_DIR=$(pixi run python -c "import os, adbc_drivers_dev; print(os.path.dirname(adbc_drivers_dev.__file__))" 2>/dev/null || true) + + if [ -n "$ADBC_DEV_DIR" ] && [ -f "$ADBC_DEV_DIR/.env" ]; then + MANYLINUX=$(grep -oP '(?<=^MANYLINUX=).*' "$ADBC_DEV_DIR/.env" || true) + RUST=$(grep -oP '(?<=^RUST=).*' "$ADBC_DEV_DIR/.env" || true) + + if [ -n "$MANYLINUX" ] && [ -n "$RUST" ]; then + IMAGE="ghcr.io/adbc-drivers/dev:${MANYLINUX}-rust${RUST}" + echo "Patching Docker image: $IMAGE" + + docker pull "$IMAGE" || true + CONTAINER_ID=$(docker run -d -u root "$IMAGE" bash -c "yum install -y perl-IPC-Cmd perl-Time-Piece && yum clean all") + EXIT_CODE=$(docker wait "$CONTAINER_ID") + if [ "$EXIT_CODE" -ne 0 ]; then + echo "Failed to install perl modules in $IMAGE" + docker logs "$CONTAINER_ID" + exit 1 + fi + docker commit "$CONTAINER_ID" "$IMAGE" + docker rm "$CONTAINER_ID" + else + echo "Failed to extract MANYLINUX or RUST from $ADBC_DEV_DIR/.env" + exit 1 + fi + else + echo "Could not find adbc_drivers_dev via pixi. Skipping patch." + exit 1 + fi fi From 98b880dd2b89d65b9c73b84d0452611b39cef3f2 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Mon, 30 Mar 2026 17:13:28 -0400 Subject: [PATCH 63/76] fix(rust): patch manylinux-rust Dockerfile rather than modifying running container --- rust/ci/scripts/pre-build.sh | 38 +++++++++++------------------------- 1 file changed, 11 insertions(+), 27 deletions(-) diff --git a/rust/ci/scripts/pre-build.sh b/rust/ci/scripts/pre-build.sh index 5b9577c..5f42072 100755 --- a/rust/ci/scripts/pre-build.sh +++ b/rust/ci/scripts/pre-build.sh @@ -6,37 +6,21 @@ if [[ "$2" == "windows" ]]; then choco install openssl -y echo "OPENSSL_DIR='C:\Program Files\OpenSSL'" > .env.build elif [[ "$2" == "linux" ]]; then - echo "Patching adbc-drivers-dev manylinux-rust image to include perl modules for openssl vendored build" + echo "Checking if we need to patch adbc-drivers-dev manylinux-rust Dockerfile" + # Ensure pixi environment is set up so we can find adbc_drivers_dev pixi install - # Find adbc_drivers_dev directory via pixi - ADBC_DEV_DIR=$(pixi run python -c "import os, adbc_drivers_dev; print(os.path.dirname(adbc_drivers_dev.__file__))" 2>/dev/null || true) + ADBC_DEV_DIR=$(pixi run python -c "import os, adbc_drivers_dev; print(os.path.dirname(adbc_drivers_dev.__file__))") + DOCKERFILE="$ADBC_DEV_DIR/compose/manylinux-rust/Dockerfile" + COMPOSEFILE="$ADBC_DEV_DIR/compose.yaml" - if [ -n "$ADBC_DEV_DIR" ] && [ -f "$ADBC_DEV_DIR/.env" ]; then - MANYLINUX=$(grep -oP '(?<=^MANYLINUX=).*' "$ADBC_DEV_DIR/.env" || true) - RUST=$(grep -oP '(?<=^RUST=).*' "$ADBC_DEV_DIR/.env" || true) + if [ -f "$DOCKERFILE" ] && [ -f "$COMPOSEFILE" ]; then + echo "Patching $DOCKERFILE to include perl modules for openssl vendored build" + sed -i 's/wget openssl openssl-devel openssl-static/wget openssl openssl-devel openssl-static perl-IPC-Cmd perl-Time-Piece/g' "$DOCKERFILE" - if [ -n "$MANYLINUX" ] && [ -n "$RUST" ]; then - IMAGE="ghcr.io/adbc-drivers/dev:${MANYLINUX}-rust${RUST}" - echo "Patching Docker image: $IMAGE" - - docker pull "$IMAGE" || true - CONTAINER_ID=$(docker run -d -u root "$IMAGE" bash -c "yum install -y perl-IPC-Cmd perl-Time-Piece && yum clean all") - EXIT_CODE=$(docker wait "$CONTAINER_ID") - if [ "$EXIT_CODE" -ne 0 ]; then - echo "Failed to install perl modules in $IMAGE" - docker logs "$CONTAINER_ID" - exit 1 - fi - docker commit "$CONTAINER_ID" "$IMAGE" - docker rm "$CONTAINER_ID" - else - echo "Failed to extract MANYLINUX or RUST from $ADBC_DEV_DIR/.env" - exit 1 - fi - else - echo "Could not find adbc_drivers_dev via pixi. Skipping patch." - exit 1 + echo "Patching $COMPOSEFILE to use local image tag" + # Change the image tag so docker compose doesn't try to pull it from ghcr.io and instead builds it locally + sed -i 's/image: ghcr.io\/adbc-drivers\/dev/image: local-patched\/adbc-drivers-dev/g' "$COMPOSEFILE" fi fi From 2064f2ed176cc7d71395e68095865df6c14765fb Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Mon, 30 Mar 2026 17:28:18 -0400 Subject: [PATCH 64/76] fix(rust): patch compose.yaml to set HOME=/tmp for protoc-installer inside manylinux-rust container --- rust/Cargo.lock | 108 ++++++++++++++++++----------------- rust/ci/scripts/pre-build.sh | 4 +- 2 files changed, 59 insertions(+), 53 deletions(-) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 7d95825..7647691 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -123,7 +123,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -134,7 +134,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -581,9 +581,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.39.0" +version = "0.39.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa7e52a4c5c547c741610a2c6f123f3881e409b714cd27e6798ef020c514f0a" +checksum = "83a25cf98105baa966497416dbd42565ce3a8cf8dbfd59803ec9ad46f3126399" dependencies = [ "cc", "cmake", @@ -1048,9 +1048,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.57" +version = "1.2.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" dependencies = [ "find-msvc-tools", "jobserver", @@ -1530,7 +1530,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -2276,9 +2276,9 @@ checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "iri-string" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8e7418f59cc01c88316161279a7f665217ae316b388e58a0d10e29f54f1e5eb" +checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20" dependencies = [ "memchr", "serde", @@ -2292,7 +2292,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -2361,10 +2361,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.91" +version = "0.3.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +checksum = "cc4c90f45aa2e6eacbe8645f77fdea542ac97a494bcd117a67df9ff4d611f995" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -2600,9 +2602,9 @@ dependencies = [ [[package]] name = "mio" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" dependencies = [ "libc", "wasi", @@ -2617,17 +2619,17 @@ checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" [[package]] name = "native-tls" -version = "0.2.18" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" dependencies = [ "libc", "log", "openssl", - "openssl-probe", + "openssl-probe 0.1.6", "openssl-sys", "schannel", - "security-framework 3.7.0", + "security-framework 2.11.1", "security-framework-sys", "tempfile", ] @@ -2648,7 +2650,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -2778,6 +2780,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + [[package]] name = "openssl-probe" version = "0.2.1" @@ -3126,7 +3134,7 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7" dependencies = [ - "heck 0.5.0", + "heck 0.4.1", "itertools 0.14.0", "log", "multimap", @@ -3451,9 +3459,9 @@ dependencies = [ [[package]] name = "rustc-hash" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" [[package]] name = "rustc_version" @@ -3483,7 +3491,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -3520,7 +3528,7 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ - "openssl-probe", + "openssl-probe 0.2.1", "rustls-pki-types", "schannel", "security-framework 3.7.0", @@ -3878,9 +3886,9 @@ checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" [[package]] name = "simd-adler32" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "simdutf8" @@ -3943,7 +3951,7 @@ version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1c97747dbf44bb1ca44a561ece23508e99cb592e862f22222dcf42f51d1e451" dependencies = [ - "heck 0.5.0", + "heck 0.4.1", "proc-macro2", "quote", "syn 2.0.117", @@ -3966,7 +3974,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4086,7 +4094,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4576,9 +4584,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.22.0" +version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -4654,9 +4662,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.114" +version = "0.2.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +checksum = "6523d69017b7633e396a89c5efab138161ed5aafcbc8d3e5c5a42ae38f50495a" dependencies = [ "cfg-if", "once_cell", @@ -4667,23 +4675,19 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.64" +version = "0.4.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +checksum = "2d1faf851e778dfa54db7cd438b70758eba9755cb47403f3496edd7c8fc212f0" dependencies = [ - "cfg-if", - "futures-util", "js-sys", - "once_cell", "wasm-bindgen", - "web-sys", ] [[package]] name = "wasm-bindgen-macro" -version = "0.2.114" +version = "0.2.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +checksum = "4e3a6c758eb2f701ed3d052ff5737f5bfe6614326ea7f3bbac7156192dc32e67" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4691,9 +4695,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.114" +version = "0.2.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +checksum = "921de2737904886b52bcbb237301552d05969a6f9c40d261eb0533c8b055fedf" dependencies = [ "bumpalo", "proc-macro2", @@ -4704,9 +4708,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.114" +version = "0.2.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +checksum = "a93e946af942b58934c604527337bad9ae33ba1d5c6900bbb41c2c07c2364a93" dependencies = [ "unicode-ident", ] @@ -4747,9 +4751,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.91" +version = "0.3.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +checksum = "84cde8507f4d7cfcb1185b8cb5890c494ffea65edbe1ba82cfd63661c805ed94" dependencies = [ "js-sys", "wasm-bindgen", @@ -4780,7 +4784,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] @@ -5238,18 +5242,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.47" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.47" +version = "0.8.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", diff --git a/rust/ci/scripts/pre-build.sh b/rust/ci/scripts/pre-build.sh index 5f42072..b11814f 100755 --- a/rust/ci/scripts/pre-build.sh +++ b/rust/ci/scripts/pre-build.sh @@ -19,8 +19,10 @@ elif [[ "$2" == "linux" ]]; then echo "Patching $DOCKERFILE to include perl modules for openssl vendored build" sed -i 's/wget openssl openssl-devel openssl-static/wget openssl openssl-devel openssl-static perl-IPC-Cmd perl-Time-Piece/g' "$DOCKERFILE" - echo "Patching $COMPOSEFILE to use local image tag" + echo "Patching $COMPOSEFILE to use local image tag and set HOME=/tmp" # Change the image tag so docker compose doesn't try to pull it from ghcr.io and instead builds it locally sed -i 's/image: ghcr.io\/adbc-drivers\/dev/image: local-patched\/adbc-drivers-dev/g' "$COMPOSEFILE" + # Inject HOME=/tmp so that rust crates (like protoc) can write to cache directories when running as non-root user + sed -i 's/^ volumes:/ environment:\n - HOME=\/tmp\n volumes:/' "$COMPOSEFILE" fi fi From c379a06656f005139ddc6dbefc8fda82779ec5b1 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Mon, 30 Mar 2026 18:01:24 -0400 Subject: [PATCH 65/76] fix(rust): exclude extraneous exported symbols on linux --- rust/Cargo.toml | 1 + rust/build.rs | 6 ++++++ 2 files changed, 7 insertions(+) create mode 100644 rust/build.rs diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 71905dd..a549a49 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -24,6 +24,7 @@ name = "adbc-driver-snowflake" version = "0.1.0" edition = "2024" +build = "build.rs" [lib] crate-type = ["cdylib", "rlib"] diff --git a/rust/build.rs b/rust/build.rs new file mode 100644 index 0000000..6d56ab2 --- /dev/null +++ b/rust/build.rs @@ -0,0 +1,6 @@ +fn main() { + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); + if target_os == "linux" || target_os == "android" { + println!("cargo:rustc-link-arg=-Wl,--exclude-libs,ALL"); + } +} From 781db22b4c3e4bfd08169ff8175230d4ebc86c44 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Mon, 30 Mar 2026 19:18:31 -0400 Subject: [PATCH 66/76] chore(rust): fix CI package generation by adding cargo-about configuration - Correct artifact name in CI from `libadbc_snowflake.*` to `libadbc_driver_snowflake.*`. - Add `rust/license.tpl` (converted to Handlebars for cargo-about). - Add `rust/about.toml` with `[[...clarify.files]]` overrides for un-licensed snowflake local workspace/git dependencies and accept list for various open-source licenses. --- .github/workflows/rust_test.yaml | 2 +- rust/Cargo.toml | 1 + rust/about.toml | 41 ++++++ rust/license.tpl | 228 +++++++++++++++++++++++++++++++ 4 files changed, 271 insertions(+), 1 deletion(-) create mode 100644 rust/about.toml create mode 100644 rust/license.tpl diff --git a/.github/workflows/rust_test.yaml b/.github/workflows/rust_test.yaml index 3f3cdb4..199675c 100644 --- a/.github/workflows/rust_test.yaml +++ b/.github/workflows/rust_test.yaml @@ -410,7 +410,7 @@ jobs: - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: drivers-${{ matrix.platform }}-${{ matrix.arch }} - path: rust/target/release/libadbc_snowflake.* + path: rust/target/release/libadbc_driver_snowflake.* retention-days: 2 package: diff --git a/rust/Cargo.toml b/rust/Cargo.toml index a549a49..c9ba3e6 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -24,6 +24,7 @@ name = "adbc-driver-snowflake" version = "0.1.0" edition = "2024" +license = "Apache-2.0" build = "build.rs" [lib] diff --git a/rust/about.toml b/rust/about.toml new file mode 100644 index 0000000..0c1fd74 --- /dev/null +++ b/rust/about.toml @@ -0,0 +1,41 @@ +accepted = [ + "Apache-2.0", + "MIT", + "ISC", + "BSD-3-Clause", + "Zlib", + "Unicode-3.0", + "MPL-2.0", + "CC0-1.0", + "CDLA-Permissive-2.0" +] + +[error_trace.clarify] +license = "Apache-2.0" +[[error_trace.clarify.files]] +path = "../LICENSE" +checksum = "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30" + +[error_trace_derive.clarify] +license = "Apache-2.0" +[[error_trace_derive.clarify.files]] +path = "../LICENSE" +checksum = "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30" + +[proto_generator.clarify] +license = "Apache-2.0" +[[proto_generator.clarify.files]] +path = "../LICENSE" +checksum = "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30" + +[proto_utils.clarify] +license = "Apache-2.0" +[[proto_utils.clarify.files]] +path = "../LICENSE" +checksum = "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30" + +[sf_core.clarify] +license = "Apache-2.0" +[[sf_core.clarify.files]] +path = "../LICENSE" +checksum = "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30" diff --git a/rust/license.tpl b/rust/license.tpl new file mode 100644 index 0000000..68d9adb --- /dev/null +++ b/rust/license.tpl @@ -0,0 +1,228 @@ +{{! + License template for binary artifacts. + + To use: + cargo about generate ./license.tpl > ./LICENSE.txt +}} + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +-------------------------------------------------------------------------------- + +This project includes code from Apache Arrow ADBC. + +Copyright: 2022 The Apache Software Foundation. +Home page: https://arrow.apache.org/ +License: http://www.apache.org/licenses/LICENSE-2.0 + +{{#each licenses}} +{{#each used_by}} +-------------------------------------------------------------------------------- + +3rdparty dependency {{crate.name}} ({{crate.version}}) +is statically linked in certain binary distributions, like the Python wheels. +{{crate.name}} is under the {{../name}} license. +{{/each}} +{{#if (not (eq name "Apache-2.0"))}} +{{text}} +{{/if}} +{{/each}} From e2500ba0cda216d7cd9e6e1747e4bf29d87984db Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Mon, 30 Mar 2026 19:28:11 -0400 Subject: [PATCH 67/76] ci(rust): install cargo-about for package generation --- .github/workflows/rust_test.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/rust_test.yaml b/.github/workflows/rust_test.yaml index 199675c..2bd8fc0 100644 --- a/.github/workflows/rust_test.yaml +++ b/.github/workflows/rust_test.yaml @@ -456,6 +456,11 @@ jobs: pattern: "drivers-*" path: "~/drivers" + - name: Install cargo-about + run: | + mkdir -p ~/.cargo/bin + curl -LsSf https://github.com/EmbarkStudios/cargo-about/releases/download/0.8.4/cargo-about-0.8.4-x86_64-unknown-linux-musl.tar.gz | tar zxf - -C ~/.cargo/bin --strip-components=1 cargo-about-0.8.4-x86_64-unknown-linux-musl/cargo-about + - name: Generate packages working-directory: rust run: | From 4199cd8b3bcde67ae4b0d3cf6e7482e2cd6a89b0 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Mon, 30 Mar 2026 20:59:40 -0400 Subject: [PATCH 68/76] ci(rust): add debug logging to package generation step --- .github/workflows/rust_test.yaml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/rust_test.yaml b/.github/workflows/rust_test.yaml index 2bd8fc0..724a75f 100644 --- a/.github/workflows/rust_test.yaml +++ b/.github/workflows/rust_test.yaml @@ -466,6 +466,11 @@ jobs: run: | pixi install + echo "=== Debug: listing ~/drivers ===" + ls -laR ~/drivers/ || echo "~/drivers does not exist" + echo "=== Debug: glob expansion ===" + echo ~/drivers/drivers-*-*/ + pixi run adbc-gen-package \ --name snowflake \ --root $(pwd) \ @@ -474,7 +479,7 @@ jobs: -o ~/packages \ ~/drivers/drivers-*-*/ - ls ~/packages + ls -laR ~/packages - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: From ce59e708ba671ff53421c845103c97e11afeb8fd Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Mon, 30 Mar 2026 21:17:48 -0400 Subject: [PATCH 69/76] fix(ci): use rust/build/ for driver artifact upload path The adbc-make check command copies the built .so to rust/build/, not rust/target/release/. The previous path only captured .rlib and .d files, causing empty package generation downstream. --- .github/workflows/rust_test.yaml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/rust_test.yaml b/.github/workflows/rust_test.yaml index 724a75f..b9ab6cc 100644 --- a/.github/workflows/rust_test.yaml +++ b/.github/workflows/rust_test.yaml @@ -410,7 +410,7 @@ jobs: - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: drivers-${{ matrix.platform }}-${{ matrix.arch }} - path: rust/target/release/libadbc_driver_snowflake.* + path: rust/build/libadbc_driver_snowflake.* retention-days: 2 package: @@ -466,11 +466,6 @@ jobs: run: | pixi install - echo "=== Debug: listing ~/drivers ===" - ls -laR ~/drivers/ || echo "~/drivers does not exist" - echo "=== Debug: glob expansion ===" - echo ~/drivers/drivers-*-*/ - pixi run adbc-gen-package \ --name snowflake \ --root $(pwd) \ From 329b0028875397af80a80f5626dcb4619382b2c8 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Mon, 30 Mar 2026 22:31:29 -0400 Subject: [PATCH 70/76] ci(rust): add test_package.py for package smoke test --- rust/ci/test_package.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 rust/ci/test_package.py diff --git a/rust/ci/test_package.py b/rust/ci/test_package.py new file mode 100644 index 0000000..fa23906 --- /dev/null +++ b/rust/ci/test_package.py @@ -0,0 +1,26 @@ +# Copyright (c) 2025 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import adbc_driver_manager.dbapi +import pytest + + +def test_package() -> None: + uri = "snowflake://example:foo@nonexistent/test" + # Just ensure the driver itself loads + with pytest.raises( + adbc_driver_manager.dbapi.ProgrammingError, match="failed to auth" + ): + with adbc_driver_manager.dbapi.connect(driver="snowflake", uri=uri): + pass From 66e354117eec8ee89e1a598a078c0d9a384a4101 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Tue, 31 Mar 2026 10:51:57 -0400 Subject: [PATCH 71/76] fix(ci): update test_package.py regex to match actual error message The Rust driver returns 'UNAUTHENTICATED: Failed to login' not 'failed to auth'. --- rust/ci/test_package.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust/ci/test_package.py b/rust/ci/test_package.py index fa23906..eeceda7 100644 --- a/rust/ci/test_package.py +++ b/rust/ci/test_package.py @@ -20,7 +20,7 @@ def test_package() -> None: uri = "snowflake://example:foo@nonexistent/test" # Just ensure the driver itself loads with pytest.raises( - adbc_driver_manager.dbapi.ProgrammingError, match="failed to auth" + adbc_driver_manager.dbapi.ProgrammingError, match="(?i)failed to (auth|login)|UNAUTHENTICATED" ): with adbc_driver_manager.dbapi.connect(driver="snowflake", uri=uri): pass From d5784ca96da73a3009f132552f89ccade672c94b Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Tue, 31 Mar 2026 11:09:22 -0400 Subject: [PATCH 72/76] pre-commit --- .github/workflows/rust_test.yaml | 8 ++++---- rust/ci/scripts/pre-build.sh | 8 ++++---- rust/ci/test_package.py | 3 ++- rust/validation/README_sf_core_limitations.md | 10 +++++----- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/.github/workflows/rust_test.yaml b/.github/workflows/rust_test.yaml index b9ab6cc..1c2d1cd 100644 --- a/.github/workflows/rust_test.yaml +++ b/.github/workflows/rust_test.yaml @@ -69,7 +69,7 @@ jobs: matrix: include: - { platform: linux, arch: amd64, runner: ubuntu-latest } - - { platform: macos, arch: arm64, runner: macos-latest } + - { platform: macos, arch: arm64, runner: macos-latest } # Disabled temporarily due to openssl-sys build failures on Windows # - { platform: windows, arch: amd64, runner: windows-latest } environment: Snowflake CI @@ -260,7 +260,7 @@ jobs: source .env.test fi set +a - pixi run adbc-make build DEBUG=true VERBOSE=true DRIVER=snowflake IMPL_LANG=rust BUILD_TAGS=minicore_disabled + pixi run adbc-make build DEBUG=true VERBOSE=true DRIVER=snowflake IMPL_LANG=rust BUILD_TAGS=minicore_disabled - name: Start Test Dependencies # Can't use Docker on macOS AArch64 runners, and windows containers @@ -276,7 +276,7 @@ jobs: exit 1 fi fi - + - name: Validate if: runner.os == 'Linux' env: @@ -338,7 +338,7 @@ jobs: matrix: include: - { platform: linux, arch: amd64, runner: ubuntu-latest } - - { platform: linux, arch: arm64, runner: ubuntu-24.04-arm } + - { platform: linux, arch: arm64, runner: ubuntu-24.04-arm } - { platform: macos, arch: arm64, runner: macos-latest } # Disabled temporarily due to openssl-sys build failures on Windows # - { platform: windows, arch: amd64, runner: windows-latest } diff --git a/rust/ci/scripts/pre-build.sh b/rust/ci/scripts/pre-build.sh index b11814f..326c0fd 100755 --- a/rust/ci/scripts/pre-build.sh +++ b/rust/ci/scripts/pre-build.sh @@ -7,18 +7,18 @@ if [[ "$2" == "windows" ]]; then echo "OPENSSL_DIR='C:\Program Files\OpenSSL'" > .env.build elif [[ "$2" == "linux" ]]; then echo "Checking if we need to patch adbc-drivers-dev manylinux-rust Dockerfile" - + # Ensure pixi environment is set up so we can find adbc_drivers_dev pixi install - + ADBC_DEV_DIR=$(pixi run python -c "import os, adbc_drivers_dev; print(os.path.dirname(adbc_drivers_dev.__file__))") DOCKERFILE="$ADBC_DEV_DIR/compose/manylinux-rust/Dockerfile" COMPOSEFILE="$ADBC_DEV_DIR/compose.yaml" - + if [ -f "$DOCKERFILE" ] && [ -f "$COMPOSEFILE" ]; then echo "Patching $DOCKERFILE to include perl modules for openssl vendored build" sed -i 's/wget openssl openssl-devel openssl-static/wget openssl openssl-devel openssl-static perl-IPC-Cmd perl-Time-Piece/g' "$DOCKERFILE" - + echo "Patching $COMPOSEFILE to use local image tag and set HOME=/tmp" # Change the image tag so docker compose doesn't try to pull it from ghcr.io and instead builds it locally sed -i 's/image: ghcr.io\/adbc-drivers\/dev/image: local-patched\/adbc-drivers-dev/g' "$COMPOSEFILE" diff --git a/rust/ci/test_package.py b/rust/ci/test_package.py index eeceda7..2f1fdc4 100644 --- a/rust/ci/test_package.py +++ b/rust/ci/test_package.py @@ -20,7 +20,8 @@ def test_package() -> None: uri = "snowflake://example:foo@nonexistent/test" # Just ensure the driver itself loads with pytest.raises( - adbc_driver_manager.dbapi.ProgrammingError, match="(?i)failed to (auth|login)|UNAUTHENTICATED" + adbc_driver_manager.dbapi.ProgrammingError, + match="(?i)failed to (auth|login)|UNAUTHENTICATED", ): with adbc_driver_manager.dbapi.connect(driver="snowflake", uri=uri): pass diff --git a/rust/validation/README_sf_core_limitations.md b/rust/validation/README_sf_core_limitations.md index 1d3c2ec..5e76355 100644 --- a/rust/validation/README_sf_core_limitations.md +++ b/rust/validation/README_sf_core_limitations.md @@ -5,10 +5,10 @@ During the implementation and validation of the Rust ADBC Snowflake driver, we e This document serves as a reference for future improvements that should ideally be made upstream in `sf_core` or Snowflake itself. ## 1. Arrow IPC Format and `client_app_id` -**Issue:** Snowflake's backend determines the default `queryResultFormat` (Arrow IPC vs JSON rowset) based on the `CLIENT_APP_ID` field sent during the login request. Unrecognized client IDs fall back to JSON rowset. +**Issue:** Snowflake's backend determines the default `queryResultFormat` (Arrow IPC vs JSON rowset) based on the `CLIENT_APP_ID` field sent during the login request. Unrecognized client IDs fall back to JSON rowset. **Impact:** JSON rowset format truncates `FLOAT`/`REAL` values to ~10 significant digits (losing IEEE 754 double precision) and converts `DBL_MAX` to `inf`. **Workaround in Driver:** The driver currently omits overriding `client_app_id` (leaving it as `sf_core`'s default `"PythonConnector"`). This tricks Snowflake into recognizing the client and returning true Arrow IPC payloads with full-precision floats. -**Ideal Fix:** +**Ideal Fix:** - **Snowflake backend:** Honor `ALTER SESSION SET QUERY_RESULT_FORMAT = 'ARROW_FORCE'` uniformly, regardless of `CLIENT_APP_ID`. - **sf_core:** Expose a native way to request Arrow format reliably without depending on a specific `CLIENT_APP_ID`, or ensure that custom client IDs can explicitly opt into Arrow format. @@ -16,12 +16,12 @@ This document serves as a reference for future improvements that should ideally **Issue:** When binding `Float64` parameters, Snowflake relies on string representation if using standard substitution. The Go driver (`gosnowflake`) formats float64 bind parameters with `FormatFloat(..., 32)` (float32 precision, ~7 significant digits), permanently truncating `3.141592653589793` to `"3.1415927"`. **Impact:** Test expectations written for the Go driver expect truncated bind values. **Workaround in Driver:** The Rust driver uses `format!("{v:?}")` for `Float64` (preserving full precision). For `Float32`, it avoids casting to `f64` first to prevent precision expansion (e.g. `3.14159` → `3.141590118408203`), preserving exact float32 semantics. -**Ideal Fix:** +**Ideal Fix:** - **Go Driver / gosnowflake:** Stop truncating float64 bind parameters to 32-bit precision. - **sf_core:** Implement native Arrow batch binding (via the Arrow IPC streaming endpoint) rather than relying on SQL string substitution for bind parameters. ## 3. Timestamp Decoding (Epoch + Fraction) -**Issue:** `sf_core` decodes `TIMESTAMP_NTZ`/`TIMESTAMP_TZ` columns as a struct containing `epoch` (seconds, Int64) and `fraction` (nanoseconds, Int32/Int64). For pre-1970 timestamps (negative epochs), Snowflake uses floor division (e.g., `-9223372037` seconds + `145224192` nanoseconds), meaning the fraction is always positive. +**Issue:** `sf_core` decodes `TIMESTAMP_NTZ`/`TIMESTAMP_TZ` columns as a struct containing `epoch` (seconds, Int64) and `fraction` (nanoseconds, Int32/Int64). For pre-1970 timestamps (negative epochs), Snowflake uses floor division (e.g., `-9223372037` seconds + `145224192` nanoseconds), meaning the fraction is always positive. **Impact:** Calculating total nanoseconds requires careful sign handling. If the fraction is subtracted when the epoch is negative (standard C-style truncation logic), the resulting date is off by ~1 second. **Workaround in Driver:** The `ConvertingReader` must explicitly add the fraction regardless of the epoch's sign: `epoch * 1_000_000_000 + fraction * frac_to_ns`. **Ideal Fix:** @@ -38,7 +38,7 @@ This document serves as a reference for future improvements that should ideally **Issue:** If `ArrowArrayStreamReader::from_raw` fails on an FFI stream exported by `sf_core`, the Arrow C Data Interface specification states that the release callback is *not* called. **Impact:** If the driver doesn't handle the error, the `Box::into_raw` pointer leaks. **Workaround in Driver:** We catch the error in `map_err`, reconstruct the `Box` using `Box::from_raw`, and explicitly `drop` it. -**Ideal Fix:** +**Ideal Fix:** - **Arrow Crate:** Provide a safer abstraction for FFI stream consumption that guarantees cleanup on failure. ## 6. Timezone Handling in Arrow IPC From 0a634fb4258c39386b150ae2882cbf9c3dfdb976 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Tue, 31 Mar 2026 11:17:41 -0400 Subject: [PATCH 73/76] fix licenses --- .rat-excludes | 6 ++++ rust/.cargo/config.toml | 14 +++++++++ rust/.gitattributes | 14 +++++++++ rust/.gitignore | 14 +++++++++ rust/Cargo.toml | 26 ++++++---------- rust/about.toml | 14 +++++++++ rust/build.rs | 14 +++++++++ rust/ci/scripts/pre-build.sh | 13 ++++++++ rust/src/connection.rs | 26 ++++++---------- rust/src/database.rs | 26 ++++++---------- rust/src/driver.rs | 28 +++++++---------- rust/src/error.rs | 26 ++++++---------- rust/src/get_objects.rs | 26 ++++++---------- rust/src/ingest.rs | 26 ++++++---------- rust/src/lib.rs | 26 ++++++---------- rust/src/statement.rs | 26 ++++++---------- rust/tests/integration.rs | 30 +++++++------------ rust/validation/README_sf_core_limitations.md | 16 ++++++++++ 18 files changed, 198 insertions(+), 173 deletions(-) diff --git a/.rat-excludes b/.rat-excludes index 2ce365a..2296e28 100644 --- a/.rat-excludes +++ b/.rat-excludes @@ -16,9 +16,15 @@ .gitmodules */go.sum */pixi.lock +*/Cargo.lock */*.csproj */*.sln go/license.tpl go/validation/queries/*/*.json go/validation/queries/*/*.sql +rust/license.tpl +rust/validation/queries/*/*.json +rust/validation/queries/*/*.sql +rust/validation/queries/*/*/*.json +rust/validation/queries/*/*/*.sql csharp/test/Interop/Resources/snowflakeconfig.json diff --git a/rust/.cargo/config.toml b/rust/.cargo/config.toml index 5e8fa28..e7ac05c 100644 --- a/rust/.cargo/config.toml +++ b/rust/.cargo/config.toml @@ -1,3 +1,17 @@ +# Copyright (c) 2026 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + # Disable dynamic linking to avoid linker errors from sf_core's dylib crate-type # when building test binaries on Linux. [build] diff --git a/rust/.gitattributes b/rust/.gitattributes index 887a2c1..402a99b 100644 --- a/rust/.gitattributes +++ b/rust/.gitattributes @@ -1,2 +1,16 @@ +# Copyright (c) 2026 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + # SCM syntax highlighting & preventing 3-way merges pixi.lock merge=binary linguist-language=YAML linguist-generated=true diff --git a/rust/.gitignore b/rust/.gitignore index ae849e6..a11c36d 100644 --- a/rust/.gitignore +++ b/rust/.gitignore @@ -1,3 +1,17 @@ +# Copyright (c) 2026 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + # pixi environments .pixi/* !.pixi/config.toml diff --git a/rust/Cargo.toml b/rust/Cargo.toml index c9ba3e6..3622301 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -1,24 +1,16 @@ # Copyright (c) 2026 ADBC Drivers Contributors # -# This file has been modified from its original version, which is -# under the Apache License: +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 # -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. [package] name = "adbc-driver-snowflake" diff --git a/rust/about.toml b/rust/about.toml index 0c1fd74..963567c 100644 --- a/rust/about.toml +++ b/rust/about.toml @@ -1,3 +1,17 @@ +# Copyright (c) 2026 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + accepted = [ "Apache-2.0", "MIT", diff --git a/rust/build.rs b/rust/build.rs index 6d56ab2..ca9d4a7 100644 --- a/rust/build.rs +++ b/rust/build.rs @@ -1,3 +1,17 @@ +// Copyright (c) 2026 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + fn main() { let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); if target_os == "linux" || target_os == "android" { diff --git a/rust/ci/scripts/pre-build.sh b/rust/ci/scripts/pre-build.sh index 326c0fd..9ca8b33 100755 --- a/rust/ci/scripts/pre-build.sh +++ b/rust/ci/scripts/pre-build.sh @@ -1,4 +1,17 @@ #!/usr/bin/env bash +# Copyright (c) 2026 ADBC Drivers Contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. set -ex diff --git a/rust/src/connection.rs b/rust/src/connection.rs index d5eddbf..94bd028 100644 --- a/rust/src/connection.rs +++ b/rust/src/connection.rs @@ -1,24 +1,16 @@ // Copyright (c) 2026 ADBC Drivers Contributors // -// This file has been modified from its original version, which is -// under the Apache License: +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at // -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 // -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. // src/connection.rs diff --git a/rust/src/database.rs b/rust/src/database.rs index 72c6f0a..7f407b2 100644 --- a/rust/src/database.rs +++ b/rust/src/database.rs @@ -1,24 +1,16 @@ // Copyright (c) 2026 ADBC Drivers Contributors // -// This file has been modified from its original version, which is -// under the Apache License: +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at // -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 // -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. // src/database.rs use std::collections::HashMap; diff --git a/rust/src/driver.rs b/rust/src/driver.rs index a0766d2..8b522f1 100644 --- a/rust/src/driver.rs +++ b/rust/src/driver.rs @@ -1,32 +1,24 @@ // Copyright (c) 2026 ADBC Drivers Contributors // -// This file has been modified from its original version, which is -// under the Apache License: +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at // -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 // -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. // src/driver.rs use std::sync::Arc; use adbc_core::{ - Optionable, error::{Error, Result, Status}, options::{OptionDatabase, OptionValue}, + Optionable, }; use arrow_schema::TimeUnit; use sf_core::apis::database_driver_v1::DatabaseDriverV1; diff --git a/rust/src/error.rs b/rust/src/error.rs index 94dbf0c..98c6dc8 100644 --- a/rust/src/error.rs +++ b/rust/src/error.rs @@ -1,24 +1,16 @@ // Copyright (c) 2026 ADBC Drivers Contributors // -// This file has been modified from its original version, which is -// under the Apache License: +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at // -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 // -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. // src/error.rs use adbc_core::error::{Error, Status}; diff --git a/rust/src/get_objects.rs b/rust/src/get_objects.rs index 1735f45..2544a57 100644 --- a/rust/src/get_objects.rs +++ b/rust/src/get_objects.rs @@ -1,24 +1,16 @@ // Copyright (c) 2026 ADBC Drivers Contributors // -// This file has been modified from its original version, which is -// under the Apache License: +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at // -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 // -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. // src/get_objects.rs diff --git a/rust/src/ingest.rs b/rust/src/ingest.rs index 37333a8..22edc13 100644 --- a/rust/src/ingest.rs +++ b/rust/src/ingest.rs @@ -1,24 +1,16 @@ // Copyright (c) 2026 ADBC Drivers Contributors // -// This file has been modified from its original version, which is -// under the Apache License: +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at // -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 // -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. // src/ingest.rs // diff --git a/rust/src/lib.rs b/rust/src/lib.rs index 287dbd4..f38c2cf 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -1,24 +1,16 @@ // Copyright (c) 2026 ADBC Drivers Contributors // -// This file has been modified from its original version, which is -// under the Apache License: +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at // -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 // -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. // src/lib.rs mod error; diff --git a/rust/src/statement.rs b/rust/src/statement.rs index 6d7aa73..06aef76 100644 --- a/rust/src/statement.rs +++ b/rust/src/statement.rs @@ -1,24 +1,16 @@ // Copyright (c) 2026 ADBC Drivers Contributors // -// This file has been modified from its original version, which is -// under the Apache License: +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at // -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 // -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. // src/statement.rs use std::sync::Arc; diff --git a/rust/tests/integration.rs b/rust/tests/integration.rs index 8de800a..d7bceb9 100644 --- a/rust/tests/integration.rs +++ b/rust/tests/integration.rs @@ -1,33 +1,25 @@ // Copyright (c) 2026 ADBC Drivers Contributors // -// This file has been modified from its original version, which is -// under the Apache License: +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at // -// Licensed to the Apache Software Foundation (ASF) under one -// or more contributor license agreements. See the NOTICE file -// distributed with this work for additional information -// regarding copyright ownership. The ASF licenses this file -// to you under the Apache License, Version 2.0 (the -// "License"); you may not use this file except in compliance -// with the License. You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 // -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. // tests/integration.rs use adbc_core::{ - Connection as _, Database as _, Driver as _, Optionable, Statement as _, options::{OptionConnection, OptionDatabase, OptionValue}, + Connection as _, Database as _, Driver as _, Optionable, Statement as _, }; use adbc_driver_snowflake::{Database, Driver}; -use arrow_array::Array; use arrow_array::cast::AsArray; +use arrow_array::Array; use arrow_schema::{DataType, TimeUnit}; fn get_env(key: &str) -> Option { diff --git a/rust/validation/README_sf_core_limitations.md b/rust/validation/README_sf_core_limitations.md index 5e76355..0c058a1 100644 --- a/rust/validation/README_sf_core_limitations.md +++ b/rust/validation/README_sf_core_limitations.md @@ -1,3 +1,19 @@ + + # Snowflake and sf_core Limitations in the Rust ADBC Driver During the implementation and validation of the Rust ADBC Snowflake driver, we encountered several architectural limitations in both the Snowflake backend and the `sf_core` library. The driver currently employs workarounds for these issues. From 16db848ca8c59bf9397a013f121b1707a7fc7be3 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Tue, 31 Mar 2026 11:20:58 -0400 Subject: [PATCH 74/76] cargo fmt --- rust/src/driver.rs | 2 +- rust/tests/integration.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/rust/src/driver.rs b/rust/src/driver.rs index 8b522f1..90a1139 100644 --- a/rust/src/driver.rs +++ b/rust/src/driver.rs @@ -16,9 +16,9 @@ use std::sync::Arc; use adbc_core::{ + Optionable, error::{Error, Result, Status}, options::{OptionDatabase, OptionValue}, - Optionable, }; use arrow_schema::TimeUnit; use sf_core::apis::database_driver_v1::DatabaseDriverV1; diff --git a/rust/tests/integration.rs b/rust/tests/integration.rs index d7bceb9..04bfcf0 100644 --- a/rust/tests/integration.rs +++ b/rust/tests/integration.rs @@ -14,12 +14,12 @@ // tests/integration.rs use adbc_core::{ - options::{OptionConnection, OptionDatabase, OptionValue}, Connection as _, Database as _, Driver as _, Optionable, Statement as _, + options::{OptionConnection, OptionDatabase, OptionValue}, }; use adbc_driver_snowflake::{Database, Driver}; -use arrow_array::cast::AsArray; use arrow_array::Array; +use arrow_array::cast::AsArray; use arrow_schema::{DataType, TimeUnit}; fn get_env(key: &str) -> Option { From c441cf24cdfa17b42297237e759c4f29d7536a26 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Fri, 3 Apr 2026 13:24:57 -0400 Subject: [PATCH 75/76] feat: use native sf_core bindings, surface query_id, add type support Replace client-side SQL parameter substitution with sf_core native JSON bindings (BindingType::Json), reducing N round-trips to 1 for multi-row parameterized queries. Surface query_id from ExecuteResult as a readable statement option (adbc.snowflake.sql.query_id) for debugging and RESULT_SCAN patterns. Add parameter binding support for: Binary, BinaryView, LargeBinary, FixedSizeBinary, Utf8View, Date32/64, Time32/Time64, Timestamp (all units, with/without timezone), and Decimal128. Additional fixes from code review: - Fix decimal128_to_string sign loss for negative values in (-1,0) - Respect NanosecondsErrorOnOverflow in ConvertingReader - Extend is_ddl with GRANT/REVOKE/SHOW/USE/DESCRIBE/DESC - Percent-decode URI query parameter values - Fix about.toml LICENSE paths, pre-build.sh idempotency Validation: 125 passed -> 143 passed, 28 skipped -> 9 skipped --- rust/Cargo.toml | 5 +- rust/about.toml | 10 +- rust/build.rs | 2 +- rust/ci/scripts/pre-build.sh | 8 +- rust/ci/test_package.py | 4 +- rust/docs/snowflake.md | 5 +- rust/src/connection.rs | 1 + rust/src/database.rs | 36 +- rust/src/statement.rs | 841 +++++++++++++----- rust/validation/queries/type/bind/binary.toml | 16 - .../queries/type/bind/binary_view.toml | 15 - rust/validation/queries/type/bind/date.toml | 16 - .../validation/queries/type/bind/decimal.toml | 15 - .../queries/type/bind/decimal.txtcase | 45 + .../queries/type/bind/fixed_size_binary.toml | 15 - .../queries/type/bind/large_binary.toml | 15 - .../queries/type/bind/string_view.toml | 15 - .../queries/type/bind/time_ms.txtcase | 19 +- .../queries/type/bind/time_ns.txtcase | 19 +- .../queries/type/bind/time_s.txtcase | 19 +- .../queries/type/bind/time_us.txtcase | 19 +- .../queries/type/bind/timestamp_ms.txtcase | 19 +- .../queries/type/bind/timestamp_ns.txtcase | 19 +- .../queries/type/bind/timestamp_s.txtcase | 19 +- .../queries/type/bind/timestamp_us.txtcase | 19 +- .../queries/type/bind/timestamptz_ms.txtcase | 19 +- .../queries/type/bind/timestamptz_ns.txtcase | 19 +- .../queries/type/bind/timestamptz_s.txtcase | 19 +- .../queries/type/bind/timestamptz_us.txtcase | 19 +- rust/validation/tests/snowflake.py | 4 +- 30 files changed, 923 insertions(+), 373 deletions(-) delete mode 100644 rust/validation/queries/type/bind/binary.toml delete mode 100644 rust/validation/queries/type/bind/binary_view.toml delete mode 100644 rust/validation/queries/type/bind/date.toml delete mode 100644 rust/validation/queries/type/bind/decimal.toml create mode 100644 rust/validation/queries/type/bind/decimal.txtcase delete mode 100644 rust/validation/queries/type/bind/fixed_size_binary.toml delete mode 100644 rust/validation/queries/type/bind/large_binary.toml delete mode 100644 rust/validation/queries/type/bind/string_view.toml diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 3622301..861c331 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -17,7 +17,6 @@ name = "adbc-driver-snowflake" version = "0.1.0" edition = "2024" license = "Apache-2.0" -build = "build.rs" [lib] crate-type = ["cdylib", "rlib"] @@ -33,7 +32,11 @@ arrow-cast = { version = "57.3.0", default-features = false } log = "0.4.22" tokio = { version = "1", features = ["rt-multi-thread"] } percent-encoding = "2.3.2" +# Force vendored (static) OpenSSL for manylinux compatibility. +# This crate doesn't call openssl APIs directly; it exists to ensure +# all transitive deps link statically. Bump manually on OpenSSL CVEs. openssl = { version = "0.10.76", features = ["vendored"] } + [dev-dependencies] env_logger = "0.10" tracing = "0.1.44" diff --git a/rust/about.toml b/rust/about.toml index 963567c..abf2eeb 100644 --- a/rust/about.toml +++ b/rust/about.toml @@ -27,29 +27,29 @@ accepted = [ [error_trace.clarify] license = "Apache-2.0" [[error_trace.clarify.files]] -path = "../LICENSE" +path = "../LICENSE.txt" checksum = "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30" [error_trace_derive.clarify] license = "Apache-2.0" [[error_trace_derive.clarify.files]] -path = "../LICENSE" +path = "../LICENSE.txt" checksum = "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30" [proto_generator.clarify] license = "Apache-2.0" [[proto_generator.clarify.files]] -path = "../LICENSE" +path = "../LICENSE.txt" checksum = "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30" [proto_utils.clarify] license = "Apache-2.0" [[proto_utils.clarify.files]] -path = "../LICENSE" +path = "../LICENSE.txt" checksum = "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30" [sf_core.clarify] license = "Apache-2.0" [[sf_core.clarify.files]] -path = "../LICENSE" +path = "../LICENSE.txt" checksum = "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30" diff --git a/rust/build.rs b/rust/build.rs index ca9d4a7..0841528 100644 --- a/rust/build.rs +++ b/rust/build.rs @@ -13,7 +13,7 @@ // limitations under the License. fn main() { - let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); + let target_os = std::env::var("CARGO_CFG_TARGET_OS").expect("CARGO_CFG_TARGET_OS not set"); if target_os == "linux" || target_os == "android" { println!("cargo:rustc-link-arg=-Wl,--exclude-libs,ALL"); } diff --git a/rust/ci/scripts/pre-build.sh b/rust/ci/scripts/pre-build.sh index 9ca8b33..dd582de 100755 --- a/rust/ci/scripts/pre-build.sh +++ b/rust/ci/scripts/pre-build.sh @@ -33,9 +33,11 @@ elif [[ "$2" == "linux" ]]; then sed -i 's/wget openssl openssl-devel openssl-static/wget openssl openssl-devel openssl-static perl-IPC-Cmd perl-Time-Piece/g' "$DOCKERFILE" echo "Patching $COMPOSEFILE to use local image tag and set HOME=/tmp" - # Change the image tag so docker compose doesn't try to pull it from ghcr.io and instead builds it locally sed -i 's/image: ghcr.io\/adbc-drivers\/dev/image: local-patched\/adbc-drivers-dev/g' "$COMPOSEFILE" - # Inject HOME=/tmp so that rust crates (like protoc) can write to cache directories when running as non-root user - sed -i 's/^ volumes:/ environment:\n - HOME=\/tmp\n volumes:/' "$COMPOSEFILE" + grep -q 'HOME=/tmp' "$COMPOSEFILE" || \ + sed -i 's/^ volumes:/ environment:\n - HOME=\/tmp\n volumes:/' "$COMPOSEFILE" + else + echo "Could not find Dockerfile at $DOCKERFILE or compose.yaml at $COMPOSEFILE — aborting." + exit 1 fi fi diff --git a/rust/ci/test_package.py b/rust/ci/test_package.py index 2f1fdc4..b78021d 100644 --- a/rust/ci/test_package.py +++ b/rust/ci/test_package.py @@ -18,10 +18,10 @@ def test_package() -> None: uri = "snowflake://example:foo@nonexistent/test" - # Just ensure the driver itself loads + # Verify the driver loads and reaches the auth phase (not a load failure). with pytest.raises( adbc_driver_manager.dbapi.ProgrammingError, - match="(?i)failed to (auth|login)|UNAUTHENTICATED", + match="(?i)(?:failed to (?:auth|login)|unauthenticated)", ): with adbc_driver_manager.dbapi.connect(driver="snowflake", uri=uri): pass diff --git a/rust/docs/snowflake.md b/rust/docs/snowflake.md index b352eb3..083afe7 100644 --- a/rust/docs/snowflake.md +++ b/rust/docs/snowflake.md @@ -12,7 +12,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -{} --- {{ cross_reference|safe }} @@ -109,8 +108,6 @@ Examples: ## Previous Versions -To see documentation for previous versions of this driver, see the following: - -- [v1.10.0](./v1.10.0.md) +There are no previous published versions of this driver yet. [snowflake]: https://www.snowflake.com/ diff --git a/rust/src/connection.rs b/rust/src/connection.rs index 94bd028..06432fd 100644 --- a/rust/src/connection.rs +++ b/rust/src/connection.rs @@ -266,6 +266,7 @@ impl adbc_core::Connection for Connection { use_high_precision: self.use_high_precision, timestamp_precision: self.timestamp_precision, bound_batches: vec![], + last_query_id: None, }) } diff --git a/rust/src/database.rs b/rust/src/database.rs index 7f407b2..efacaa0 100644 --- a/rust/src/database.rs +++ b/rust/src/database.rs @@ -329,8 +329,22 @@ impl Database { if let Some(info) = user_info { if let Some(colon) = info.find(':') { - let user = percent_decode_str(&info[..colon]).decode_utf8_lossy(); - let pass = percent_decode_str(&info[colon + 1..]).decode_utf8_lossy(); + let user = percent_decode_str(&info[..colon]) + .decode_utf8() + .map_err(|e| { + Error::with_message_and_status( + &format!("invalid UTF-8 in URI username: {e}"), + Status::InvalidArguments, + ) + })?; + let pass = percent_decode_str(&info[colon + 1..]) + .decode_utf8() + .map_err(|e| { + Error::with_message_and_status( + &format!("invalid UTF-8 in URI password: {e}"), + Status::InvalidArguments, + ) + })?; if !user.is_empty() { self.set_option( OptionDatabase::Username, @@ -342,7 +356,12 @@ impl Database { OptionValue::String(pass.into_owned()), )?; } else if !info.is_empty() { - let user = percent_decode_str(&info).decode_utf8_lossy(); + let user = percent_decode_str(&info).decode_utf8().map_err(|e| { + Error::with_message_and_status( + &format!("invalid UTF-8 in URI username: {e}"), + Status::InvalidArguments, + ) + })?; self.set_option( OptionDatabase::Username, OptionValue::String(user.into_owned()), @@ -380,7 +399,14 @@ impl Database { for pair in q.split('&') { if let Some(eq) = pair.find('=') { let k = &pair[..eq]; - let v = &pair[eq + 1..]; + let v = percent_decode_str(&pair[eq + 1..]) + .decode_utf8() + .map_err(|e| { + Error::with_message_and_status( + &format!("invalid UTF-8 in URI query parameter: {e}"), + Status::InvalidArguments, + ) + })?; let adbc_key = match k { "warehouse" => "adbc.snowflake.sql.warehouse", "role" => "adbc.snowflake.sql.role", @@ -396,7 +422,7 @@ impl Database { }; self.set_option( OptionDatabase::Other(adbc_key.into()), - OptionValue::String(v.to_string()), + OptionValue::String(v.into_owned()), )?; } } diff --git a/rust/src/statement.rs b/rust/src/statement.rs index 06aef76..d8928f1 100644 --- a/rust/src/statement.rs +++ b/rust/src/statement.rs @@ -22,7 +22,7 @@ use adbc_core::{ }; use arrow_array::{Array, ArrayRef, RecordBatch, RecordBatchReader}; use arrow_schema::{DataType, Field, Schema, TimeUnit}; -use sf_core::apis::database_driver_v1::Handle; +use sf_core::apis::database_driver_v1::{BindingType, DataPtr, Handle}; use crate::driver::{Inner, TimestampPrecision}; @@ -40,6 +40,7 @@ pub struct Statement { pub(crate) timestamp_precision: TimestampPrecision, /// Parameter batches stored by bind() / bind_stream(). Each row is one execution. pub(crate) bound_batches: Vec, + pub(crate) last_query_id: Option, } impl Drop for Statement { @@ -137,6 +138,14 @@ impl Optionable for Statement { OptionStatement::Other(ref k) if k == "adbc.snowflake.statement.query_tag" => { Ok(self.query_tag.clone().unwrap_or_default()) } + OptionStatement::Other(ref k) if k == "adbc.snowflake.sql.query_id" => { + self.last_query_id.clone().ok_or_else(|| { + Error::with_message_and_status( + "no query has been executed yet", + Status::NotFound, + ) + }) + } _ => Err(Error::with_message_and_status( format!("option not found: {}", key.as_ref()), Status::NotFound, @@ -167,65 +176,47 @@ impl Optionable for Statement { } impl Statement { - /// Execute a parameterized query once per row of every bound batch. - /// Parameter values are substituted directly as SQL literals — this avoids - /// relying on sf_core's JSON binding path and works with all Snowflake - /// server versions without session configuration. - fn execute_bound(&self, query: String) -> Result> { - let mut all_batches: Vec = Vec::new(); - let mut result_schema: Option> = None; - - for bound_batch in &self.bound_batches { - for row_idx in 0..bound_batch.num_rows() { - let bound_sql = substitute_params(&query, bound_batch, row_idx)?; - - let result = self - .inner - .runtime - .block_on(async { - self.inner - .sf - .statement_set_sql_query(self.stmt_handle, bound_sql) - .await?; - self.inner - .sf - .statement_execute_query(self.stmt_handle, None) - .await - }) - .map_err(crate::error::api_error_to_adbc_error)?; - - // Safety: same as execute(). - let raw = Box::into_raw(result.stream) - as *mut arrow_array::ffi_stream::FFI_ArrowArrayStream; - let reader = - unsafe { arrow_array::ffi_stream::ArrowArrayStreamReader::from_raw(raw) } - .map_err(|e| { - // Safety: Arrow's C Data Interface specifies that on failure, from_raw - // does NOT call the stream's release callback, so reconstructing the - // Box here is the only release path — no double-free risk. - drop(unsafe { Box::from_raw(raw) }); - Error::with_message_and_status(e.to_string(), Status::IO) - })?; - - if result_schema.is_none() { - result_schema = Some(reader.schema()); - } - for batch in reader { - let batch = batch - .map_err(|e| Error::with_message_and_status(e.to_string(), Status::IO))?; - all_batches.push(batch); - } - } - } + /// Execute a parameterized query with all bound rows sent as JSON bindings + /// in a single round-trip via sf_core's `BindingType::Json` API. + fn execute_bound( + &mut self, + query: String, + ) -> Result> { + let json_bytes = arrow_batches_to_json_bindings(&self.bound_batches)?; + let data_ptr = DataPtr::new(json_bytes.as_ptr(), json_bytes.len() as i64); + let binding = BindingType::Json(data_ptr); - let schema = result_schema.unwrap_or_else(|| Arc::new(Schema::empty())); + let result = self + .inner + .runtime + .block_on(async { + self.inner + .sf + .statement_set_sql_query(self.stmt_handle, query) + .await?; + self.inner + .sf + .statement_execute_query(self.stmt_handle, Some(binding)) + .await + }) + .map_err(crate::error::api_error_to_adbc_error)?; + + self.last_query_id = Some(result.query_id.clone()); + + // Safety: result.stream is a valid FFI stream from sf_core. Ownership is transferred + // to ArrowArrayStreamReader. The C ABI layout is stable per the Arrow C Data Interface. + let raw = + Box::into_raw(result.stream) as *mut arrow_array::ffi_stream::FFI_ArrowArrayStream; + let reader = unsafe { arrow_array::ffi_stream::ArrowArrayStreamReader::from_raw(raw) } + .map_err(|e| { + drop(unsafe { Box::from_raw(raw) }); + Error::with_message_and_status(e.to_string(), Status::IO) + })?; Ok(Box::new(ConvertingReader::new( - ConcatReader { - batches: all_batches.into_iter(), - schema, - }, + reader, self.use_high_precision, self.timestamp_precision.time_unit(), + self.timestamp_precision, ))) } @@ -289,7 +280,6 @@ impl adbc_core::Statement for Statement { self.apply_query_tag()?; - // If parameters are bound, execute once per row and concatenate results. if !self.bound_batches.is_empty() { return self.execute_bound(query); } @@ -309,15 +299,15 @@ impl adbc_core::Statement for Statement { }) .map_err(crate::error::api_error_to_adbc_error)?; + self.last_query_id = Some(result.query_id.clone()); + // Safety: result.stream is a valid FFI stream from sf_core. Ownership is transferred // to ArrowArrayStreamReader. The C ABI layout is stable per the Arrow C Data Interface. let raw = Box::into_raw(result.stream) as *mut arrow_array::ffi_stream::FFI_ArrowArrayStream; let reader = unsafe { arrow_array::ffi_stream::ArrowArrayStreamReader::from_raw(raw) } .map_err(|e| { - // Safety: Arrow's C Data Interface specifies that on failure, from_raw - // does NOT call the stream's release callback, so reconstructing the - // Box here is the only release path — no double-free risk. + // Safety: on failure, from_raw does NOT call the stream's release callback. drop(unsafe { Box::from_raw(raw) }); Error::with_message_and_status(e.to_string(), Status::IO) })?; @@ -325,6 +315,7 @@ impl adbc_core::Statement for Statement { reader, self.use_high_precision, self.timestamp_precision.time_unit(), + self.timestamp_precision, ))) } @@ -340,30 +331,35 @@ impl adbc_core::Statement for Statement { self.apply_query_tag()?; - // Parameterised DML: execute once per bound row and sum row counts. + // Parameterised DML: execute once with JSON bindings. if !self.bound_batches.is_empty() { - let mut total: i64 = 0; - for bound_batch in &self.bound_batches { - for row_idx in 0..bound_batch.num_rows() { - let sql = substitute_params(&query, bound_batch, row_idx)?; - let result = self - .inner - .runtime - .block_on(async { - self.inner - .sf - .statement_set_sql_query(self.stmt_handle, sql) - .await?; - self.inner - .sf - .statement_execute_query(self.stmt_handle, None) - .await - }) - .map_err(crate::error::api_error_to_adbc_error)?; - total += result.rows_affected.unwrap_or(0); - } - } - return Ok(if is_ddl(&query) { None } else { Some(total) }); + let json_bytes = arrow_batches_to_json_bindings(&self.bound_batches)?; + let data_ptr = DataPtr::new(json_bytes.as_ptr(), json_bytes.len() as i64); + let binding = BindingType::Json(data_ptr); + + let result = self + .inner + .runtime + .block_on(async { + self.inner + .sf + .statement_set_sql_query(self.stmt_handle, query) + .await?; + self.inner + .sf + .statement_execute_query(self.stmt_handle, Some(binding)) + .await + }) + .map_err(crate::error::api_error_to_adbc_error)?; + + self.last_query_id = Some(result.query_id.clone()); + + let rows = if is_ddl(self.query.as_deref().unwrap_or("")) { + None + } else { + result.rows_affected + }; + return Ok(rows); } let result = self @@ -381,6 +377,8 @@ impl adbc_core::Statement for Statement { }) .map_err(crate::error::api_error_to_adbc_error)?; + self.last_query_id = Some(result.query_id.clone()); + // DDL statements (CREATE, DROP, ALTER, TRUNCATE) return a non-meaningful row // count from Snowflake (typically 1 for "success"). Per the ADBC convention, // return None (-1 in Python) for DDL so callers can distinguish it from DML. @@ -414,20 +412,17 @@ impl adbc_core::Statement for Statement { }) .map_err(crate::error::api_error_to_adbc_error)?; + self.last_query_id = Some(result.query_id.clone()); + // Safety: result.stream is a valid FFI stream from sf_core. Ownership is transferred // to ArrowArrayStreamReader. The C ABI layout is stable per the Arrow C Data Interface. let raw = Box::into_raw(result.stream) as *mut arrow_array::ffi_stream::FFI_ArrowArrayStream; let reader = unsafe { arrow_array::ffi_stream::ArrowArrayStreamReader::from_raw(raw) } .map_err(|e| { - // Safety: Arrow's C Data Interface specifies that on failure, from_raw - // does NOT call the stream's release callback, so reconstructing the - // Box here is the only release path — no double-free risk. drop(unsafe { Box::from_raw(raw) }); Error::with_message_and_status(e.to_string(), Status::IO) })?; - // .schema() calls get_schema on the FFI stream without consuming any record batches. - // Dropping the reader invokes the stream's release callback. Ok(adjust_schema( &reader.schema(), self.use_high_precision, @@ -475,11 +470,13 @@ impl adbc_core::Statement for Statement { // ── ConcatReader: chains multiple RecordBatches into a single reader ────────── +#[cfg(test)] struct ConcatReader { batches: std::vec::IntoIter, schema: Arc, } +#[cfg(test)] impl Iterator for ConcatReader { type Item = std::result::Result; fn next(&mut self) -> Option { @@ -487,6 +484,7 @@ impl Iterator for ConcatReader { } } +#[cfg(test)] impl RecordBatchReader for ConcatReader { fn schema(&self) -> Arc { self.schema.clone() @@ -622,12 +620,18 @@ pub(crate) struct ConvertingReader { schema: Arc, use_high_precision: bool, ts_unit: TimeUnit, + check_overflow: bool, logical_types: Vec, scales: Vec, } impl ConvertingReader { - pub(crate) fn new(inner: R, use_high_precision: bool, ts_unit: TimeUnit) -> Self { + pub(crate) fn new( + inner: R, + use_high_precision: bool, + ts_unit: TimeUnit, + ts_precision: TimestampPrecision, + ) -> Self { let orig_schema = inner.schema(); let logical_types: Vec = orig_schema .fields() @@ -645,11 +649,13 @@ impl ConvertingReader { }) .collect(); let schema = adjust_schema(&orig_schema, use_high_precision, ts_unit); + Self { inner, schema, use_high_precision, ts_unit, + check_overflow: ts_precision == TimestampPrecision::NanosecondsErrorOnOverflow, logical_types, scales, } @@ -1005,10 +1011,7 @@ impl Iterator for ConvertingReader { Err(e) => return Some(Err(e)), }; - // check_overflow is intentionally false: overflow truncation is accepted - // for far-future/far-past timestamps at nanosecond precision. Callers - // that need strict overflow detection should use a separate validation step. - let check_overflow = false; + let check_overflow = self.check_overflow; let adjusted_columns: std::result::Result, arrow_schema::ArrowError> = batch .columns() @@ -1046,150 +1049,388 @@ impl RecordBatchReader for ConvertingReader { } } -// ── Parameter substitution ──────────────────────────────────────────────────── - -/// Replaces each `?` placeholder in `query` with the SQL literal value of the -/// corresponding bound column at `row_idx`. -/// -/// Skips `?` inside SQL string literals (`'…'`), line comments (`--…`), and -/// block comments (`/*…*/`) so only true parameter markers are substituted. -/// Returns `InvalidArguments` if there are more `?` markers than bound columns. -fn substitute_params(query: &str, batch: &RecordBatch, row_idx: usize) -> Result { - let mut result = String::with_capacity(query.len() * 2); - let mut param_idx = 0usize; - let mut chars = query.chars().peekable(); - - while let Some(ch) = chars.next() { - match ch { - // SQL string literal — copy verbatim; '' is an escaped quote (stay in string) - '\'' => { - result.push('\''); - loop { - match chars.next() { - None => break, - Some('\'') => { - result.push('\''); - if chars.peek() == Some(&'\'') { - result.push(chars.next().unwrap()); // escaped '' - } else { - break; // end of string - } - } - Some(c) => result.push(c), - } - } +// ── JSON parameter bindings ─────────────────────────────────────────────────── + +fn snowflake_type_name(dt: &DataType) -> Result<&'static str> { + match dt { + DataType::Int8 + | DataType::Int16 + | DataType::Int32 + | DataType::Int64 + | DataType::UInt8 + | DataType::UInt16 + | DataType::UInt32 + | DataType::UInt64 + | DataType::Decimal128(_, _) => Ok("FIXED"), + DataType::Float32 | DataType::Float64 => Ok("REAL"), + DataType::Utf8 | DataType::LargeUtf8 | DataType::Utf8View => Ok("TEXT"), + DataType::Boolean => Ok("BOOLEAN"), + DataType::Date32 | DataType::Date64 => Ok("DATE"), + DataType::Binary + | DataType::LargeBinary + | DataType::FixedSizeBinary(_) + | DataType::BinaryView => Ok("BINARY"), + DataType::Time32(_) | DataType::Time64(_) => Ok("TIME"), + DataType::Timestamp(_, None) => Ok("TIMESTAMP_NTZ"), + DataType::Timestamp(_, Some(_)) => Ok("TIMESTAMP_LTZ"), + _ => Err(Error::with_message_and_status( + format!("unsupported bind parameter type: {dt:?}"), + Status::NotImplemented, + )), + } +} + +fn escape_json_string(s: &str) -> String { + let mut out = String::with_capacity(s.len()); + for c in s.chars() { + match c { + '"' => out.push_str("\\\""), + '\\' => out.push_str("\\\\"), + '\n' => out.push_str("\\n"), + '\r' => out.push_str("\\r"), + '\t' => out.push_str("\\t"), + c if (c as u32) < 0x20 => { + out.push_str(&format!("\\u{:04x}", c as u32)); } - // Line comment -- copy until end of line - '-' if chars.peek() == Some(&'-') => { - result.push('-'); - result.push(chars.next().unwrap()); - for c in chars.by_ref() { - result.push(c); - if c == '\n' { - break; - } + c => out.push(c), + } + } + out +} + +fn arrow_batches_to_json_bindings(batches: &[RecordBatch]) -> Result { + if batches.is_empty() { + return Ok("{}".to_string()); + } + + let num_cols = batches[0].num_columns(); + let schema = batches[0].schema(); + + let mut col_types: Vec<&'static str> = Vec::with_capacity(num_cols); + for i in 0..num_cols { + col_types.push(snowflake_type_name(schema.field(i).data_type())?); + } + + let total_rows: usize = batches.iter().map(|b| b.num_rows()).sum(); + + let mut col_values: Vec>> = vec![Vec::with_capacity(total_rows); num_cols]; + + for batch in batches { + for col_idx in 0..num_cols { + let col = batch.column(col_idx); + let dt = col.data_type(); + for row in 0..batch.num_rows() { + if col.is_null(row) { + col_values[col_idx].push(None); + continue; } + let val = format_arrow_value(col.as_ref(), row, dt)?; + col_values[col_idx].push(val); } - // Block comment /* … */ — copy verbatim - '/' if chars.peek() == Some(&'*') => { - result.push('/'); - result.push(chars.next().unwrap()); - let mut prev = '\0'; - for c in chars.by_ref() { - result.push(c); - if prev == '*' && c == '/' { - break; - } - prev = c; + } + } + + let mut json = String::from("{"); + for col_idx in 0..num_cols { + if col_idx > 0 { + json.push(','); + } + let key = col_idx + 1; + json.push_str(&format!( + "\"{}\":{{\"type\":\"{}\",\"value\":", + key, col_types[col_idx] + )); + + if total_rows == 1 { + match &col_values[col_idx][0] { + None => json.push_str("null"), + Some(v) => { + json.push('"'); + json.push_str(&escape_json_string(v)); + json.push('"'); } } - // Parameter placeholder - '?' => { - if param_idx >= batch.num_columns() { - return Err(Error::with_message_and_status( - format!( - "query has more '?' placeholders than bound columns (have {})", - batch.num_columns() - ), - Status::InvalidArguments, - )); + } else { + json.push('['); + for (i, v) in col_values[col_idx].iter().enumerate() { + if i > 0 { + json.push(','); + } + match v { + None => json.push_str("null"), + Some(s) => { + json.push('"'); + json.push_str(&escape_json_string(s)); + json.push('"'); + } } - let col = batch.column(param_idx); - result.push_str(&arrow_value_to_sql_literal(col.as_ref(), row_idx)?); - param_idx += 1; } - c => result.push(c), + json.push(']'); } + json.push('}'); } - Ok(result) + json.push('}'); + Ok(json) } -/// Formats an Arrow column value at `row` as a Snowflake SQL literal. -/// NULL → `NULL`; strings are single-quoted; numbers are unquoted. -fn arrow_value_to_sql_literal(arr: &dyn Array, row: usize) -> Result { - if arr.is_null(row) { - return Ok("NULL".to_string()); - } - use arrow_array::{ - BooleanArray, Date32Array, Int16Array, Int32Array, Int64Array, LargeStringArray, - StringArray, - }; - macro_rules! num_lit { - ($T:ty) => { - if let Some(a) = arr.as_any().downcast_ref::<$T>() { - return Ok(format!("{}", a.value(row))); +fn format_arrow_value(arr: &dyn Array, row: usize, dt: &DataType) -> Result> { + use arrow_array::*; + match dt { + DataType::Int8 => Ok(Some( + arr.as_any() + .downcast_ref::() + .unwrap() + .value(row) + .to_string(), + )), + DataType::Int16 => Ok(Some( + arr.as_any() + .downcast_ref::() + .unwrap() + .value(row) + .to_string(), + )), + DataType::Int32 => Ok(Some( + arr.as_any() + .downcast_ref::() + .unwrap() + .value(row) + .to_string(), + )), + DataType::Int64 => Ok(Some( + arr.as_any() + .downcast_ref::() + .unwrap() + .value(row) + .to_string(), + )), + DataType::UInt8 => Ok(Some( + arr.as_any() + .downcast_ref::() + .unwrap() + .value(row) + .to_string(), + )), + DataType::UInt16 => Ok(Some( + arr.as_any() + .downcast_ref::() + .unwrap() + .value(row) + .to_string(), + )), + DataType::UInt32 => Ok(Some( + arr.as_any() + .downcast_ref::() + .unwrap() + .value(row) + .to_string(), + )), + DataType::UInt64 => Ok(Some( + arr.as_any() + .downcast_ref::() + .unwrap() + .value(row) + .to_string(), + )), + DataType::Float32 => { + let v = arr + .as_any() + .downcast_ref::() + .unwrap() + .value(row); + if v.is_finite() { + Ok(Some(format!("{v:?}"))) + } else { + Ok(None) } - }; - } - // Floats need {:?} format which always emits a decimal point or exponent - // (e.g. "3.14", "1.7976931348623157e308"). The {} Display format may produce - // a huge integer string (e.g. "179769300...") that Snowflake rejects. - if let Some(a) = arr.as_any().downcast_ref::() { - let v = a.value(row); - return if v.is_finite() { - Ok(format!("{v:?}")) - } else { - Ok("NULL".to_string()) - }; - } - if let Some(a) = arr.as_any().downcast_ref::() { - let v = a.value(row); - return if v.is_finite() { - Ok(format!("{v:?}")) - } else { - Ok("NULL".to_string()) - }; - } - num_lit!(Int64Array); - num_lit!(Int32Array); - num_lit!(Int16Array); - if let Some(a) = arr.as_any().downcast_ref::() { - return Ok(sql_str_lit(a.value(row))); - } - if let Some(a) = arr.as_any().downcast_ref::() { - return Ok(sql_str_lit(a.value(row))); - } - if let Some(a) = arr.as_any().downcast_ref::() { - return Ok(if a.value(row) { "TRUE" } else { "FALSE" }.to_string()); - } - if let Some(a) = arr.as_any().downcast_ref::() { - return Ok(format!( - "'{}'::DATE", - days_since_epoch_to_date_str(a.value(row) as i64) - )); + } + DataType::Float64 => { + let v = arr + .as_any() + .downcast_ref::() + .unwrap() + .value(row); + if v.is_finite() { + Ok(Some(format!("{v:?}"))) + } else { + Ok(None) + } + } + DataType::Utf8 => Ok(Some( + arr.as_any() + .downcast_ref::() + .unwrap() + .value(row) + .to_string(), + )), + DataType::LargeUtf8 => Ok(Some( + arr.as_any() + .downcast_ref::() + .unwrap() + .value(row) + .to_string(), + )), + DataType::Boolean => Ok(Some( + arr.as_any() + .downcast_ref::() + .unwrap() + .value(row) + .to_string(), + )), + DataType::Date32 => { + let days = arr + .as_any() + .downcast_ref::() + .unwrap() + .value(row) as i64; + Ok(Some((days * 86_400_000).to_string())) + } + DataType::Date64 => { + let ms = arr + .as_any() + .downcast_ref::() + .unwrap() + .value(row); + Ok(Some(ms.to_string())) + } + DataType::Decimal128(_, scale) => { + let val = arr + .as_any() + .downcast_ref::() + .unwrap() + .value(row); + Ok(Some(decimal128_to_string(val, *scale))) + } + DataType::Binary => { + let bytes = arr + .as_any() + .downcast_ref::() + .unwrap() + .value(row); + Ok(Some(bytes.iter().map(|b| format!("{b:02x}")).collect())) + } + DataType::LargeBinary => { + let bytes = arr + .as_any() + .downcast_ref::() + .unwrap() + .value(row); + Ok(Some(bytes.iter().map(|b| format!("{b:02x}")).collect())) + } + DataType::FixedSizeBinary(_) => { + let bytes = arr + .as_any() + .downcast_ref::() + .unwrap() + .value(row); + Ok(Some(bytes.iter().map(|b| format!("{b:02x}")).collect())) + } + DataType::BinaryView => { + let bytes = arr + .as_any() + .downcast_ref::() + .unwrap() + .value(row); + Ok(Some(bytes.iter().map(|b| format!("{b:02x}")).collect())) + } + DataType::Utf8View => Ok(Some( + arr.as_any() + .downcast_ref::() + .unwrap() + .value(row) + .to_string(), + )), + DataType::Time32(TimeUnit::Second) => { + let v = arr + .as_any() + .downcast_ref::() + .unwrap() + .value(row) as i64; + Ok(Some((v * 1_000_000_000).to_string())) + } + DataType::Time32(TimeUnit::Millisecond) => { + let v = arr + .as_any() + .downcast_ref::() + .unwrap() + .value(row) as i64; + Ok(Some((v * 1_000_000).to_string())) + } + DataType::Time32(_) => { + unreachable!("Time32 only supports Second and Millisecond units") + } + DataType::Time64(TimeUnit::Microsecond) => { + let v = arr + .as_any() + .downcast_ref::() + .unwrap() + .value(row); + Ok(Some((v * 1_000).to_string())) + } + DataType::Time64(TimeUnit::Nanosecond) => { + let v = arr + .as_any() + .downcast_ref::() + .unwrap() + .value(row); + Ok(Some(v.to_string())) + } + DataType::Time64(_) => { + unreachable!("Time64 only supports Microsecond and Nanosecond units") + } + DataType::Timestamp(unit, _) => { + let (v, multiplier) = match unit { + TimeUnit::Second => ( + arr.as_any() + .downcast_ref::() + .unwrap() + .value(row), + 1_000_000_000i64, + ), + TimeUnit::Millisecond => ( + arr.as_any() + .downcast_ref::() + .unwrap() + .value(row), + 1_000_000i64, + ), + TimeUnit::Microsecond => ( + arr.as_any() + .downcast_ref::() + .unwrap() + .value(row), + 1_000i64, + ), + TimeUnit::Nanosecond => ( + arr.as_any() + .downcast_ref::() + .unwrap() + .value(row), + 1i64, + ), + }; + Ok(Some((v as i128 * multiplier as i128).to_string())) + } + _ => Err(Error::with_message_and_status( + format!("unsupported bind parameter type: {dt:?}"), + Status::NotImplemented, + )), } - Err(Error::with_message_and_status( - format!("unsupported bind parameter type: {:?}", arr.data_type()), - Status::NotImplemented, - )) } -/// Wraps `s` in single quotes. -/// Backslashes are doubled first (some Snowflake sessions treat `\'` as an escape -/// sequence, which would prematurely close the literal), then single quotes are -/// doubled per ANSI SQL. -fn sql_str_lit(s: &str) -> String { - format!("'{}'", s.replace('\\', "\\\\").replace('\'', "''")) +fn decimal128_to_string(value: i128, scale: i8) -> String { + if scale <= 0 { + return value.to_string(); + } + let sign = if value < 0 { "-" } else { "" }; + let abs = value.unsigned_abs(); + let divisor = 10u128.pow(scale as u32); + let integer_part = abs / divisor; + let fractional_part = abs % divisor; + format!( + "{sign}{integer_part}.{fractional_part:0>width$}", + width = scale as usize + ) } /// Converts days since Unix epoch (1970-01-01) to a YYYY-MM-DD string. @@ -1236,6 +1477,12 @@ fn is_ddl(query: &str) -> bool { || upper.starts_with("TRUNCATE ") || upper.starts_with("RENAME ") || upper.starts_with("COMMENT ") + || upper.starts_with("GRANT ") + || upper.starts_with("REVOKE ") + || upper.starts_with("SHOW ") + || upper.starts_with("USE ") + || upper.starts_with("DESCRIBE ") + || upper.starts_with("DESC ") } #[cfg(test)] @@ -1258,6 +1505,7 @@ mod tests { use_high_precision: true, timestamp_precision: TimestampPrecision::Nanoseconds, bound_batches: vec![], + last_query_id: None, } } @@ -1302,6 +1550,7 @@ mod tests { use_high_precision: true, timestamp_precision: TimestampPrecision::Nanoseconds, bound_batches: vec![], + last_query_id: None, }; match stmt.execute() { Err(err) => assert_eq!(err.status, adbc_core::error::Status::InvalidState), @@ -1325,6 +1574,7 @@ mod tests { use_high_precision: true, timestamp_precision: TimestampPrecision::Nanoseconds, bound_batches: vec![], + last_query_id: None, }; stmt.set_sql_query("SELECT 1").unwrap(); assert!(stmt.target_table.is_none()); @@ -1443,7 +1693,12 @@ mod tests { batches: vec![batch].into_iter(), schema, }; - let mut cr = ConvertingReader::new(reader, true, TimeUnit::Nanosecond); + let mut cr = ConvertingReader::new( + reader, + true, + TimeUnit::Nanosecond, + TimestampPrecision::Nanoseconds, + ); let out = cr.next().unwrap().unwrap(); assert_eq!(out.schema().field(0).data_type(), &DataType::Int64); let col = out @@ -1464,7 +1719,12 @@ mod tests { batches: vec![batch].into_iter(), schema, }; - let mut cr = ConvertingReader::new(reader, true, TimeUnit::Nanosecond); + let mut cr = ConvertingReader::new( + reader, + true, + TimeUnit::Nanosecond, + TimestampPrecision::Nanoseconds, + ); let out = cr.next().unwrap().unwrap(); let col = out .column(0) @@ -1484,7 +1744,12 @@ mod tests { batches: vec![batch].into_iter(), schema, }; - let mut cr = ConvertingReader::new(reader, true, TimeUnit::Nanosecond); + let mut cr = ConvertingReader::new( + reader, + true, + TimeUnit::Nanosecond, + TimestampPrecision::Nanoseconds, + ); let out = cr.next().unwrap().unwrap(); assert_eq!(out.schema().field(0).data_type(), &DataType::Int64); assert_eq!(out.num_rows(), 0); @@ -1525,7 +1790,12 @@ mod tests { batches: vec![batch1, batch2].into_iter(), schema: declared_schema, }; - let mut cr = ConvertingReader::new(reader, true, TimeUnit::Nanosecond); + let mut cr = ConvertingReader::new( + reader, + true, + TimeUnit::Nanosecond, + TimestampPrecision::Nanoseconds, + ); let out1 = cr.next().unwrap().unwrap(); assert_eq!(out1.column(0).data_type(), &DataType::Int64); @@ -1636,7 +1906,12 @@ mod tests { batches: vec![batch].into_iter(), schema, }; - let mut cr = ConvertingReader::new(reader, false, TimeUnit::Nanosecond); + let mut cr = ConvertingReader::new( + reader, + false, + TimeUnit::Nanosecond, + TimestampPrecision::Nanoseconds, + ); let out = cr.next().unwrap().unwrap(); assert_eq!(out.schema().field(0).data_type(), &DataType::Float64); let col = out @@ -1663,7 +1938,12 @@ mod tests { batches: vec![batch].into_iter(), schema, }; - let mut cr = ConvertingReader::new(reader, true, TimeUnit::Nanosecond); + let mut cr = ConvertingReader::new( + reader, + true, + TimeUnit::Nanosecond, + TimestampPrecision::Nanoseconds, + ); let out = cr.next().unwrap().unwrap(); assert_eq!( out.schema().field(0).data_type(), @@ -1693,7 +1973,12 @@ mod tests { batches: vec![batch].into_iter(), schema, }; - let mut cr = ConvertingReader::new(reader, true, TimeUnit::Nanosecond); + let mut cr = ConvertingReader::new( + reader, + true, + TimeUnit::Nanosecond, + TimestampPrecision::Nanoseconds, + ); let out = cr.next().unwrap().unwrap(); assert_eq!(out.schema().field(0).data_type(), &DataType::Int64); } @@ -1715,7 +2000,12 @@ mod tests { batches: vec![batch].into_iter(), schema, }; - let mut cr = ConvertingReader::new(reader, true, TimeUnit::Nanosecond); + let mut cr = ConvertingReader::new( + reader, + true, + TimeUnit::Nanosecond, + TimestampPrecision::Nanoseconds, + ); let out = cr.next().unwrap().unwrap(); assert_eq!( out.schema().field(0).data_type(), @@ -1747,7 +2037,12 @@ mod tests { batches: vec![batch].into_iter(), schema, }; - let mut cr = ConvertingReader::new(reader, false, TimeUnit::Nanosecond); + let mut cr = ConvertingReader::new( + reader, + false, + TimeUnit::Nanosecond, + TimestampPrecision::Nanoseconds, + ); let out = cr.next().unwrap().unwrap(); assert_eq!(out.schema().field(0).data_type(), &DataType::Float64); let col = out @@ -1772,7 +2067,12 @@ mod tests { batches: vec![batch].into_iter(), schema, }; - let mut cr = ConvertingReader::new(reader, false, TimeUnit::Nanosecond); + let mut cr = ConvertingReader::new( + reader, + false, + TimeUnit::Nanosecond, + TimestampPrecision::Nanoseconds, + ); let out = cr.next().unwrap().unwrap(); assert_eq!( out.schema().field(0).data_type(), @@ -2005,4 +2305,81 @@ mod tests { .unwrap(); assert_eq!(ts.value(0), 1_000_000_005); } + + #[test] + fn test_arrow_batches_to_json_bindings_basic() { + let schema = Arc::new(Schema::new(vec![ + Field::new("id", DataType::Int64, false), + Field::new("name", DataType::Utf8, false), + ])); + let batch = RecordBatch::try_new( + schema, + vec![ + Arc::new(arrow_array::Int64Array::from(vec![123, 456])) as ArrayRef, + Arc::new(arrow_array::StringArray::from(vec!["hello", "world"])) as ArrayRef, + ], + ) + .unwrap(); + + let json = arrow_batches_to_json_bindings(&[batch]).unwrap(); + assert!(json.contains("\"1\":{\"type\":\"FIXED\",\"value\":[\"123\",\"456\"]}")); + assert!(json.contains("\"2\":{\"type\":\"TEXT\",\"value\":[\"hello\",\"world\"]}")); + } + + #[test] + fn test_arrow_batches_to_json_bindings_with_nulls() { + let schema = Arc::new(Schema::new(vec![ + Field::new("id", DataType::Int64, true), + Field::new("name", DataType::Utf8, true), + ])); + let batch = RecordBatch::try_new( + schema, + vec![ + Arc::new(arrow_array::Int64Array::from(vec![Some(1), None, Some(3)])) as ArrayRef, + Arc::new(arrow_array::StringArray::from(vec![ + Some("a"), + Some("b"), + None, + ])) as ArrayRef, + ], + ) + .unwrap(); + + let json = arrow_batches_to_json_bindings(&[batch]).unwrap(); + assert!(json.contains("\"1\":{\"type\":\"FIXED\",\"value\":[\"1\",null,\"3\"]}")); + assert!(json.contains("\"2\":{\"type\":\"TEXT\",\"value\":[\"a\",\"b\",null]}")); + } + + #[test] + fn test_arrow_batches_to_json_bindings_float_precision() { + let schema = Arc::new(Schema::new(vec![Field::new("v", DataType::Float64, false)])); + let batch = RecordBatch::try_new( + schema, + vec![Arc::new(arrow_array::Float64Array::from(vec![3.141592653589793])) as ArrayRef], + ) + .unwrap(); + + let json = arrow_batches_to_json_bindings(&[batch]).unwrap(); + assert!( + json.contains("3.141592653589793"), + "should preserve full f64 precision, got: {json}" + ); + } + + #[test] + fn test_decimal128_to_string_negative_fractional() { + // Values in (-1, 0) must preserve the negative sign + assert_eq!(decimal128_to_string(-50, 2), "-0.50"); + assert_eq!(decimal128_to_string(-1, 1), "-0.1"); + assert_eq!(decimal128_to_string(-999, 3), "-0.999"); + // Normal negative values + assert_eq!(decimal128_to_string(-150, 2), "-1.50"); + // Positive values unchanged + assert_eq!(decimal128_to_string(50, 2), "0.50"); + assert_eq!(decimal128_to_string(150, 2), "1.50"); + // Zero + assert_eq!(decimal128_to_string(0, 2), "0.00"); + // No scale + assert_eq!(decimal128_to_string(-42, 0), "-42"); + } } diff --git a/rust/validation/queries/type/bind/binary.toml b/rust/validation/queries/type/bind/binary.toml deleted file mode 100644 index 2a1d94f..0000000 --- a/rust/validation/queries/type/bind/binary.toml +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright (c) 2025 ADBC Drivers Contributors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Skip binary binding tests - not supported by Snowflake driver binding layer -skip = "Binary parameter binding not supported by Snowflake driver" diff --git a/rust/validation/queries/type/bind/binary_view.toml b/rust/validation/queries/type/bind/binary_view.toml deleted file mode 100644 index 097305b..0000000 --- a/rust/validation/queries/type/bind/binary_view.toml +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (c) 2025 ADBC Drivers Contributors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -skip = "binary_view parameter binding not supported by Snowflake driver" diff --git a/rust/validation/queries/type/bind/date.toml b/rust/validation/queries/type/bind/date.toml deleted file mode 100644 index f3325fb..0000000 --- a/rust/validation/queries/type/bind/date.toml +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright (c) 2025 ADBC Drivers Contributors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Skip date binding tests - not supported by Snowflake driver binding layer -skip = "Date parameter binding not supported by Snowflake driver" diff --git a/rust/validation/queries/type/bind/decimal.toml b/rust/validation/queries/type/bind/decimal.toml deleted file mode 100644 index e920682..0000000 --- a/rust/validation/queries/type/bind/decimal.toml +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (c) 2025 ADBC Drivers Contributors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -skip = "decimal parameter binding not supported by Snowflake driver" diff --git a/rust/validation/queries/type/bind/decimal.txtcase b/rust/validation/queries/type/bind/decimal.txtcase new file mode 100644 index 0000000..ee57a69 --- /dev/null +++ b/rust/validation/queries/type/bind/decimal.txtcase @@ -0,0 +1,45 @@ +// Copyright (c) 2025 ADBC Drivers Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// part: metadata + +sort-keys = [["res", "ascending"]] + +[setup] +drop = "test_decimal" + +[tags] +sql-type-name = "NUMERIC" +caveats = ["With use_high_precision=false, Snowflake returns NUMERIC as double"] + +// part: expected_schema + +{ + "format": "+s", + "children": [ + { + "name": "res", + "format": "g", + "flags": ["nullable"] + } + ] +} + +// part: expected + +{"res": null} +{"res": -999.99} +{"res": 0.0} +{"res": 123.45} +{"res": 9999999.99} diff --git a/rust/validation/queries/type/bind/fixed_size_binary.toml b/rust/validation/queries/type/bind/fixed_size_binary.toml deleted file mode 100644 index cad0d1b..0000000 --- a/rust/validation/queries/type/bind/fixed_size_binary.toml +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (c) 2025 ADBC Drivers Contributors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -skip = "fixed_size_binary parameter binding not supported by Snowflake driver" diff --git a/rust/validation/queries/type/bind/large_binary.toml b/rust/validation/queries/type/bind/large_binary.toml deleted file mode 100644 index 434a8b8..0000000 --- a/rust/validation/queries/type/bind/large_binary.toml +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (c) 2025 ADBC Drivers Contributors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -skip = "large_binary parameter binding not supported by Snowflake driver" diff --git a/rust/validation/queries/type/bind/string_view.toml b/rust/validation/queries/type/bind/string_view.toml deleted file mode 100644 index 069a563..0000000 --- a/rust/validation/queries/type/bind/string_view.toml +++ /dev/null @@ -1,15 +0,0 @@ -# Copyright (c) 2025 ADBC Drivers Contributors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -skip = "string_view parameter binding not supported by Snowflake driver" diff --git a/rust/validation/queries/type/bind/time_ms.txtcase b/rust/validation/queries/type/bind/time_ms.txtcase index 7b7b09d..e27121e 100644 --- a/rust/validation/queries/type/bind/time_ms.txtcase +++ b/rust/validation/queries/type/bind/time_ms.txtcase @@ -13,4 +13,21 @@ // limitations under the License. // part: metadata -skip = "time_ms parameter binding not supported by Snowflake driver" + +[setup] +drop = "test_time" + +[tags] +sql-type-name = "TIME" + +// part: setup_query + +CREATE OR REPLACE TABLE "test_time" ("idx" INT, "res" TIME(3)); + +// part: bind_query + +INSERT INTO "test_time" VALUES ($1, $2) + +// part: query + +SELECT "res" FROM "test_time" ORDER BY "idx" diff --git a/rust/validation/queries/type/bind/time_ns.txtcase b/rust/validation/queries/type/bind/time_ns.txtcase index e0b3e3d..7550272 100644 --- a/rust/validation/queries/type/bind/time_ns.txtcase +++ b/rust/validation/queries/type/bind/time_ns.txtcase @@ -13,4 +13,21 @@ // limitations under the License. // part: metadata -skip = "time_ns parameter binding not supported by Snowflake driver" + +[setup] +drop = "test_time" + +[tags] +sql-type-name = "TIME" + +// part: setup_query + +CREATE OR REPLACE TABLE "test_time" ("idx" INT, "res" TIME(9)); + +// part: bind_query + +INSERT INTO "test_time" VALUES ($1, $2) + +// part: query + +SELECT "res" FROM "test_time" ORDER BY "idx" diff --git a/rust/validation/queries/type/bind/time_s.txtcase b/rust/validation/queries/type/bind/time_s.txtcase index 5573ad8..198689e 100644 --- a/rust/validation/queries/type/bind/time_s.txtcase +++ b/rust/validation/queries/type/bind/time_s.txtcase @@ -13,4 +13,21 @@ // limitations under the License. // part: metadata -skip = "time_s parameter binding not supported by Snowflake driver" + +[setup] +drop = "test_time" + +[tags] +sql-type-name = "TIME" + +// part: setup_query + +CREATE OR REPLACE TABLE "test_time" ("idx" INT, "res" TIME(0)); + +// part: bind_query + +INSERT INTO "test_time" VALUES ($1, $2) + +// part: query + +SELECT "res" FROM "test_time" ORDER BY "idx" diff --git a/rust/validation/queries/type/bind/time_us.txtcase b/rust/validation/queries/type/bind/time_us.txtcase index 056d1e7..3822942 100644 --- a/rust/validation/queries/type/bind/time_us.txtcase +++ b/rust/validation/queries/type/bind/time_us.txtcase @@ -13,4 +13,21 @@ // limitations under the License. // part: metadata -skip = "time_us parameter binding not supported by Snowflake driver" + +[setup] +drop = "test_time" + +[tags] +sql-type-name = "TIME" + +// part: setup_query + +CREATE OR REPLACE TABLE "test_time" ("idx" INT, "res" TIME(6)); + +// part: bind_query + +INSERT INTO "test_time" VALUES ($1, $2) + +// part: query + +SELECT "res" FROM "test_time" ORDER BY "idx" diff --git a/rust/validation/queries/type/bind/timestamp_ms.txtcase b/rust/validation/queries/type/bind/timestamp_ms.txtcase index 897ea15..a625b1a 100644 --- a/rust/validation/queries/type/bind/timestamp_ms.txtcase +++ b/rust/validation/queries/type/bind/timestamp_ms.txtcase @@ -13,4 +13,21 @@ // limitations under the License. // part: metadata -skip = "timestamp_ms parameter binding not supported by Snowflake driver" + +[setup] +drop = "test_timestamp" + +[tags] +sql-type-name = "TIMESTAMP_NTZ" + +// part: setup_query + +CREATE OR REPLACE TABLE "test_timestamp" ("idx" INT, "res" TIMESTAMP(3)); + +// part: bind_query + +INSERT INTO "test_timestamp" VALUES ($1, $2) + +// part: query + +SELECT "res" FROM "test_timestamp" ORDER BY "idx" diff --git a/rust/validation/queries/type/bind/timestamp_ns.txtcase b/rust/validation/queries/type/bind/timestamp_ns.txtcase index 81bc30f..9fa9651 100644 --- a/rust/validation/queries/type/bind/timestamp_ns.txtcase +++ b/rust/validation/queries/type/bind/timestamp_ns.txtcase @@ -13,4 +13,21 @@ // limitations under the License. // part: metadata -skip = "timestamp_ns parameter binding not supported by Snowflake driver" + +[setup] +drop = "test_timestamp" + +[tags] +sql-type-name = "TIMESTAMP_NTZ" + +// part: setup_query + +CREATE OR REPLACE TABLE "test_timestamp" ("idx" INT, "res" TIMESTAMP(9)); + +// part: bind_query + +INSERT INTO "test_timestamp" VALUES ($1, $2) + +// part: query + +SELECT "res" FROM "test_timestamp" ORDER BY "idx" diff --git a/rust/validation/queries/type/bind/timestamp_s.txtcase b/rust/validation/queries/type/bind/timestamp_s.txtcase index 07dbb68..0e6a19a 100644 --- a/rust/validation/queries/type/bind/timestamp_s.txtcase +++ b/rust/validation/queries/type/bind/timestamp_s.txtcase @@ -13,4 +13,21 @@ // limitations under the License. // part: metadata -skip = "timestamp_s parameter binding not supported by Snowflake driver" + +[setup] +drop = "test_timestamp" + +[tags] +sql-type-name = "TIMESTAMP_NTZ" + +// part: setup_query + +CREATE OR REPLACE TABLE "test_timestamp" ("idx" INT, "res" TIMESTAMP(0)); + +// part: bind_query + +INSERT INTO "test_timestamp" VALUES ($1, $2) + +// part: query + +SELECT "res" FROM "test_timestamp" ORDER BY "idx" diff --git a/rust/validation/queries/type/bind/timestamp_us.txtcase b/rust/validation/queries/type/bind/timestamp_us.txtcase index 84f4d71..fd1464e 100644 --- a/rust/validation/queries/type/bind/timestamp_us.txtcase +++ b/rust/validation/queries/type/bind/timestamp_us.txtcase @@ -13,4 +13,21 @@ // limitations under the License. // part: metadata -skip = "timestamp_us parameter binding not supported by Snowflake driver" + +[setup] +drop = "test_timestamp" + +[tags] +sql-type-name = "TIMESTAMP_NTZ" + +// part: setup_query + +CREATE OR REPLACE TABLE "test_timestamp" ("idx" INT, "res" TIMESTAMP(6)); + +// part: bind_query + +INSERT INTO "test_timestamp" VALUES ($1, $2) + +// part: query + +SELECT "res" FROM "test_timestamp" ORDER BY "idx" diff --git a/rust/validation/queries/type/bind/timestamptz_ms.txtcase b/rust/validation/queries/type/bind/timestamptz_ms.txtcase index 468fbaa..eec6e24 100644 --- a/rust/validation/queries/type/bind/timestamptz_ms.txtcase +++ b/rust/validation/queries/type/bind/timestamptz_ms.txtcase @@ -13,4 +13,21 @@ // limitations under the License. // part: metadata -skip = "timestamptz_ms parameter binding not supported by Snowflake driver" + +[setup] +drop = "test_timestamptz" + +[tags] +sql-type-name = "TIMESTAMP_TZ" + +// part: setup_query + +CREATE OR REPLACE TABLE "test_timestamptz" ("idx" INT, "res" TIMESTAMPLTZ(3), "res2" TIMESTAMPLTZ(3)); + +// part: bind_query + +INSERT INTO "test_timestamptz" VALUES ($1, $2, $3) + +// part: query + +SELECT "res", "res2" FROM "test_timestamptz" ORDER BY "idx" diff --git a/rust/validation/queries/type/bind/timestamptz_ns.txtcase b/rust/validation/queries/type/bind/timestamptz_ns.txtcase index 71f8d1b..2b0bc30 100644 --- a/rust/validation/queries/type/bind/timestamptz_ns.txtcase +++ b/rust/validation/queries/type/bind/timestamptz_ns.txtcase @@ -13,4 +13,21 @@ // limitations under the License. // part: metadata -skip = "timestamptz_ns parameter binding not supported by Snowflake driver" + +[setup] +drop = "test_timestamptz" + +[tags] +sql-type-name = "TIMESTAMP_TZ" + +// part: setup_query + +CREATE OR REPLACE TABLE "test_timestamptz" ("idx" INT, "res" TIMESTAMPLTZ(9), "res2" TIMESTAMPLTZ(9)); + +// part: bind_query + +INSERT INTO "test_timestamptz" VALUES ($1, $2, $3) + +// part: query + +SELECT "res", "res2" FROM "test_timestamptz" ORDER BY "idx" diff --git a/rust/validation/queries/type/bind/timestamptz_s.txtcase b/rust/validation/queries/type/bind/timestamptz_s.txtcase index 77696b4..1da5e02 100644 --- a/rust/validation/queries/type/bind/timestamptz_s.txtcase +++ b/rust/validation/queries/type/bind/timestamptz_s.txtcase @@ -13,4 +13,21 @@ // limitations under the License. // part: metadata -skip = "timestamptz_s parameter binding not supported by Snowflake driver" + +[setup] +drop = "test_timestamptz" + +[tags] +sql-type-name = "TIMESTAMP_TZ" + +// part: setup_query + +CREATE OR REPLACE TABLE "test_timestamptz" ("idx" INT, "res" TIMESTAMPLTZ(0), "res2" TIMESTAMPLTZ(0)); + +// part: bind_query + +INSERT INTO "test_timestamptz" VALUES ($1, $2, $3) + +// part: query + +SELECT "res", "res2" FROM "test_timestamptz" ORDER BY "idx" diff --git a/rust/validation/queries/type/bind/timestamptz_us.txtcase b/rust/validation/queries/type/bind/timestamptz_us.txtcase index 916beb7..ab5a447 100644 --- a/rust/validation/queries/type/bind/timestamptz_us.txtcase +++ b/rust/validation/queries/type/bind/timestamptz_us.txtcase @@ -13,4 +13,21 @@ // limitations under the License. // part: metadata -skip = "timestamptz_us parameter binding not supported by Snowflake driver" + +[setup] +drop = "test_timestamptz" + +[tags] +sql-type-name = "TIMESTAMP_TZ" + +// part: setup_query + +CREATE OR REPLACE TABLE "test_timestamptz" ("idx" INT, "res" TIMESTAMPLTZ(6), "res2" TIMESTAMPLTZ(6)); + +// part: bind_query + +INSERT INTO "test_timestamptz" VALUES ($1, $2, $3) + +// part: query + +SELECT "res", "res2" FROM "test_timestamptz" ORDER BY "idx" diff --git a/rust/validation/tests/snowflake.py b/rust/validation/tests/snowflake.py index 2702c9b..fb1d929 100644 --- a/rust/validation/tests/snowflake.py +++ b/rust/validation/tests/snowflake.py @@ -82,10 +82,12 @@ def is_table_not_found(self, table_name: str | None, error: Exception) -> bool: ) def quote_one_identifier(self, identifier: str) -> str: - """Quote an identifier to preserve case and ensure consistency.""" identifier = identifier.replace('"', '""') return f'"{identifier}"' + def bind_parameter(self, index: int) -> str: + return f"${index}" + def split_statement(self, statement: str) -> list[str]: return quirks.split_statement(statement, dialect=self.name) From 7054c22c647bd1d2f6df6aade8eb53ae81f035e6 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Fri, 10 Apr 2026 14:17:44 -0400 Subject: [PATCH 76/76] update to latest version of sf_core --- rust/Cargo.lock | 501 ++++++++++++++++++++++++++++++++-------------- rust/Cargo.toml | 14 +- rust/src/error.rs | 2 + 3 files changed, 355 insertions(+), 162 deletions(-) diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 7647691..19e804e 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -8,10 +8,10 @@ version = "0.1.0" dependencies = [ "adbc_core", "adbc_ffi", - "arrow-array 57.3.0", - "arrow-buffer 57.3.0", - "arrow-cast 57.3.0", - "arrow-schema 57.3.0", + "arrow-array 58.1.0", + "arrow-buffer 58.1.0", + "arrow-cast 58.1.0", + "arrow-schema 58.1.0", "env_logger 0.10.2", "log", "openssl", @@ -24,23 +24,23 @@ dependencies = [ [[package]] name = "adbc_core" -version = "0.22.0" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8dbe031527c9856a1e2df5e82aa8e568ffaab3be897f70d874477fb42a783bb" +checksum = "46b169525a7c41670fe95874103c7c6ce713ac699123f81a200bc31f9ad3b02e" dependencies = [ - "arrow-array 57.3.0", - "arrow-schema 57.3.0", + "arrow-array 58.1.0", + "arrow-schema 58.1.0", ] [[package]] name = "adbc_ffi" -version = "0.22.0" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3600ae9aec2907516d088189e3b863029280f1953dd0eab903c7f4c862a0ce81" +checksum = "e6851c2ab953511cf7a244aadcbc0586442fd3c67dfe371457369048880dd513" dependencies = [ "adbc_core", - "arrow-array 57.3.0", - "arrow-schema 57.3.0", + "arrow-array 58.1.0", + "arrow-schema 58.1.0", ] [[package]] @@ -123,7 +123,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -134,7 +134,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -196,14 +196,14 @@ dependencies = [ [[package]] name = "arrow-array" -version = "57.3.0" +version = "58.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8955af33b25f3b175ee10af580577280b4bd01f7e823d94c7cdef7cf8c9aef" +checksum = "772bd34cacdda8baec9418d80d23d0fb4d50ef0735685bd45158b83dfeb6e62d" dependencies = [ "ahash", - "arrow-buffer 57.3.0", - "arrow-data 57.3.0", - "arrow-schema 57.3.0", + "arrow-buffer 58.1.0", + "arrow-data 58.1.0", + "arrow-schema 58.1.0", "chrono", "chrono-tz", "half", @@ -226,9 +226,9 @@ dependencies = [ [[package]] name = "arrow-buffer" -version = "57.3.0" +version = "58.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c697ddca96183182f35b3a18e50b9110b11e916d7b7799cbfd4d34662f2c56c2" +checksum = "898f4cf1e9598fdb77f356fdf2134feedfd0ee8d5a4e0a5f573e7d0aec16baa4" dependencies = [ "bytes", "half", @@ -258,16 +258,16 @@ dependencies = [ [[package]] name = "arrow-cast" -version = "57.3.0" +version = "58.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "646bbb821e86fd57189c10b4fcdaa941deaf4181924917b0daa92735baa6ada5" +checksum = "b0127816c96533d20fc938729f48c52d3e48f99717e7a0b5ade77d742510736d" dependencies = [ - "arrow-array 57.3.0", - "arrow-buffer 57.3.0", - "arrow-data 57.3.0", - "arrow-ord 57.3.0", - "arrow-schema 57.3.0", - "arrow-select 57.3.0", + "arrow-array 58.1.0", + "arrow-buffer 58.1.0", + "arrow-data 58.1.0", + "arrow-ord 58.1.0", + "arrow-schema 58.1.0", + "arrow-select 58.1.0", "atoi", "base64 0.22.1", "chrono", @@ -306,12 +306,12 @@ dependencies = [ [[package]] name = "arrow-data" -version = "57.3.0" +version = "58.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fdd994a9d28e6365aa78e15da3f3950c0fdcea6b963a12fa1c391afb637b304" +checksum = "42d10beeab2b1c3bb0b53a00f7c944a178b622173a5c7bcabc3cb45d90238df4" dependencies = [ - "arrow-buffer 57.3.0", - "arrow-schema 57.3.0", + "arrow-buffer 58.1.0", + "arrow-schema 58.1.0", "half", "num-integer", "num-traits", @@ -368,15 +368,15 @@ dependencies = [ [[package]] name = "arrow-ord" -version = "57.3.0" +version = "58.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d8f1870e03d4cbed632959498bcc84083b5a24bded52905ae1695bd29da45b" +checksum = "763a7ba279b20b52dad300e68cfc37c17efa65e68623169076855b3a9e941ca5" dependencies = [ - "arrow-array 57.3.0", - "arrow-buffer 57.3.0", - "arrow-data 57.3.0", - "arrow-schema 57.3.0", - "arrow-select 57.3.0", + "arrow-array 58.1.0", + "arrow-buffer 58.1.0", + "arrow-data 58.1.0", + "arrow-schema 58.1.0", + "arrow-select 58.1.0", ] [[package]] @@ -403,9 +403,9 @@ dependencies = [ [[package]] name = "arrow-schema" -version = "57.3.0" +version = "58.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c872d36b7bf2a6a6a2b40de9156265f0242910791db366a2c17476ba8330d68" +checksum = "c30a1365d7a7dc50cc847e54154e6af49e4c4b0fddc9f607b687f29212082743" dependencies = [ "bitflags", ] @@ -426,15 +426,15 @@ dependencies = [ [[package]] name = "arrow-select" -version = "57.3.0" +version = "58.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68bf3e3efbd1278f770d67e5dc410257300b161b93baedb3aae836144edcaf4b" +checksum = "78694888660a9e8ac949853db393af2a8b8fc82c19ce333132dfa2e72cc1a7fe" dependencies = [ "ahash", - "arrow-array 57.3.0", - "arrow-buffer 57.3.0", - "arrow-data 57.3.0", - "arrow-schema 57.3.0", + "arrow-array 58.1.0", + "arrow-buffer 58.1.0", + "arrow-data 58.1.0", + "arrow-schema 58.1.0", "num-traits", ] @@ -494,6 +494,18 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "async-compression" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0f9ee0f6e02ffd7ad5816e9464499fba7b3effd01123b515c41d1697c43dad1" +dependencies = [ + "compression-codecs", + "compression-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -621,9 +633,9 @@ dependencies = [ [[package]] name = "aws-sdk-s3" -version = "1.127.0" +version = "1.129.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "151783f64e0dcddeb4965d08e36c276b4400a46caa88805a2e36d497deaf031a" +checksum = "6d4e8410fadbc0ee453145dd77a4958227b18b05bf67c2795d0a8b8596c9aa0f" dependencies = [ "aws-credential-types", "aws-runtime", @@ -835,7 +847,7 @@ dependencies = [ "http 1.4.0", "http-body 0.4.6", "hyper 0.14.32", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-rustls 0.24.2", "hyper-rustls 0.27.7", "hyper-util", @@ -1048,9 +1060,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.58" +version = "1.2.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" dependencies = [ "find-msvc-tools", "jobserver", @@ -1160,6 +1172,23 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "compression-codecs" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb7b51a7d9c967fc26773061ba86150f19c50c0d65c887cb1fbe295fd16619b7" +dependencies = [ + "compression-core", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" + [[package]] name = "const-oid" version = "0.9.6" @@ -1530,13 +1559,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "error_trace" version = "0.1.0" -source = "git+https://github.com/snowflakedb/universal-driver?rev=42b799464edec27361668b6b18792c7cc38cb785#42b799464edec27361668b6b18792c7cc38cb785" +source = "git+https://github.com/snowflakedb/universal-driver?rev=1b8fc700c6fc10464851407da90ac5f9645758f7#1b8fc700c6fc10464851407da90ac5f9645758f7" dependencies = [ "error_trace_derive", "snafu 0.8.9", @@ -1545,7 +1574,7 @@ dependencies = [ [[package]] name = "error_trace_derive" version = "0.1.0" -source = "git+https://github.com/snowflakedb/universal-driver?rev=42b799464edec27361668b6b18792c7cc38cb785#42b799464edec27361668b6b18792c7cc38cb785" +source = "git+https://github.com/snowflakedb/universal-driver?rev=1b8fc700c6fc10464851407da90ac5f9645758f7#1b8fc700c6fc10464851407da90ac5f9645758f7" dependencies = [ "quote", "syn 2.0.117", @@ -1553,9 +1582,21 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "faststr" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ca7d44d22004409a61c393afb3369c8f7bb74abcae49fe249ee01dcc3002113" +dependencies = [ + "bytes", + "rkyv", + "serde", + "simdutf8", +] [[package]] name = "ff" @@ -1880,6 +1921,12 @@ dependencies = [ "foldhash 0.2.0", ] +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + [[package]] name = "heck" version = "0.4.1" @@ -2021,9 +2068,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" dependencies = [ "atomic-waker", "bytes", @@ -2035,7 +2082,6 @@ dependencies = [ "httparse", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -2063,7 +2109,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ "http 1.4.0", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-util", "rustls 0.23.37", "rustls-native-certs", @@ -2082,7 +2128,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", "http-body-util", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-util", "native-tls", "tokio", @@ -2102,7 +2148,7 @@ dependencies = [ "futures-util", "http 1.4.0", "http-body 1.0.1", - "hyper 1.8.1", + "hyper 1.9.0", "ipnet", "libc", "percent-encoding", @@ -2141,12 +2187,13 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -2154,9 +2201,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -2167,9 +2214,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -2181,15 +2228,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -2201,15 +2248,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -2249,12 +2296,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.0", "serde", "serde_core", ] @@ -2292,7 +2339,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2361,9 +2408,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.92" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc4c90f45aa2e6eacbe8645f77fdea542ac97a494bcd117a67df9ff4d611f995" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" dependencies = [ "cfg-if", "futures-util", @@ -2473,9 +2520,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.183" +version = "0.2.184" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" [[package]] name = "libm" @@ -2485,9 +2532,9 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ "libc", ] @@ -2510,9 +2557,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "lock_api" @@ -2617,19 +2664,39 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" +[[package]] +name = "munge" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e17401f259eba956ca16491461b6e8f72913a0a114e39736ce404410f915a0c" +dependencies = [ + "munge_macro", +] + +[[package]] +name = "munge_macro" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4568f25ccbd45ab5d5603dc34318c1ec56b117531781260002151b8530a9f931" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "native-tls" -version = "0.2.14" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" dependencies = [ "libc", "log", "openssl", - "openssl-probe 0.1.6", + "openssl-probe", "openssl-sys", "schannel", - "security-framework 2.11.1", + "security-framework 3.7.0", "security-framework-sys", "tempfile", ] @@ -2650,7 +2717,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -2780,12 +2847,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "openssl-probe" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" - [[package]] name = "openssl-probe" version = "0.2.1" @@ -2794,9 +2855,9 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-src" -version = "300.5.5+3.5.5" +version = "300.6.0+3.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f1787d533e03597a7934fd0a765f0d28e94ecc5fb7789f8053b1e699a56f709" +checksum = "a8e8cbfd3a4a8c8f089147fd7aaa33cf8c7450c4d09f8f80698a0cf093abeff4" dependencies = [ "cc", ] @@ -3057,9 +3118,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -3134,7 +3195,7 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7" dependencies = [ - "heck 0.4.1", + "heck 0.5.0", "itertools 0.14.0", "log", "multimap", @@ -3198,7 +3259,7 @@ dependencies = [ [[package]] name = "proto_generator" version = "0.1.0" -source = "git+https://github.com/snowflakedb/universal-driver?rev=42b799464edec27361668b6b18792c7cc38cb785#42b799464edec27361668b6b18792c7cc38cb785" +source = "git+https://github.com/snowflakedb/universal-driver?rev=1b8fc700c6fc10464851407da90ac5f9645758f7#1b8fc700c6fc10464851407da90ac5f9645758f7" dependencies = [ "clap", "env_logger 0.11.10", @@ -3215,7 +3276,27 @@ dependencies = [ [[package]] name = "proto_utils" version = "0.1.0" -source = "git+https://github.com/snowflakedb/universal-driver?rev=42b799464edec27361668b6b18792c7cc38cb785#42b799464edec27361668b6b18792c7cc38cb785" +source = "git+https://github.com/snowflakedb/universal-driver?rev=1b8fc700c6fc10464851407da90ac5f9645758f7#1b8fc700c6fc10464851407da90ac5f9645758f7" + +[[package]] +name = "ptr_meta" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9a0cf95a1196af61d4f1cbdab967179516d9a4a4312af1f31948f8f6224a79" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7347867d0a7e1208d93b46767be83e2b8f978c3dad35f775ac8d8847551d6fe1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] [[package]] name = "quinn" @@ -3293,6 +3374,15 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rancor" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a063ea72381527c2a0561da9c80000ef822bdd7c3241b1cc1b12100e3df081ee" +dependencies = [ + "ptr_meta", +] + [[package]] name = "rand" version = "0.9.2" @@ -3351,6 +3441,26 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "regex" version = "1.12.3" @@ -3386,6 +3496,12 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "rend" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cadadef317c2f20755a64d7fdc48f9e7178ee6b0e1f7fce33fa60f1d68a276e6" + [[package]] name = "reqwest" version = "0.12.28" @@ -3402,7 +3518,7 @@ dependencies = [ "http 1.4.0", "http-body 1.0.1", "http-body-util", - "hyper 1.8.1", + "hyper 1.9.0", "hyper-rustls 0.27.7", "hyper-tls", "hyper-util", @@ -3457,6 +3573,35 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rkyv" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a30e631b7f4a03dee9056b8ef6982e8ba371dd5bedb74d3ec86df4499132c70" +dependencies = [ + "bytes", + "hashbrown 0.16.1", + "indexmap", + "munge", + "ptr_meta", + "rancor", + "rend", + "rkyv_derive", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8100bb34c0a1d0f907143db3149e6b4eea3c33b9ee8b189720168e818303986f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "rustc-hash" version = "2.1.2" @@ -3491,7 +3636,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3517,7 +3662,7 @@ dependencies = [ "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.10", + "rustls-webpki 0.103.11", "subtle", "zeroize", ] @@ -3528,7 +3673,7 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ - "openssl-probe 0.2.1", + "openssl-probe", "rustls-pki-types", "schannel", "security-framework 3.7.0", @@ -3576,9 +3721,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.10" +version = "0.103.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +checksum = "20a6af516fea4b20eccceaf166e8aa666ac996208e8a644ce3ef5aa783bc7cd4" dependencies = [ "aws-lc-rs", "ring", @@ -3684,9 +3829,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -3755,7 +3900,7 @@ dependencies = [ [[package]] name = "sf_core" version = "0.0.0" -source = "git+https://github.com/snowflakedb/universal-driver?rev=42b799464edec27361668b6b18792c7cc38cb785#42b799464edec27361668b6b18792c7cc38cb785" +source = "git+https://github.com/snowflakedb/universal-driver?rev=1b8fc700c6fc10464851407da90ac5f9645758f7#1b8fc700c6fc10464851407da90ac5f9645758f7" dependencies = [ "arrow", "arrow-ipc", @@ -3781,6 +3926,7 @@ dependencies = [ "keyring", "libc", "lru 0.12.5", + "memchr", "num-traits", "once_cell", "openssl", @@ -3803,6 +3949,7 @@ dependencies = [ "sha2", "signature 2.2.0", "snafu 0.8.9", + "sonic-rs", "spki 0.7.3", "thiserror 1.0.69", "time", @@ -3951,7 +4098,7 @@ version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1c97747dbf44bb1ca44a561ece23508e99cb592e862f22222dcf42f51d1e451" dependencies = [ - "heck 0.4.1", + "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.117", @@ -3974,7 +4121,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", +] + +[[package]] +name = "sonic-number" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3775c3390edf958191f1ab1e8c5c188907feebd0f3ce1604cb621f72961dbf32" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "sonic-rs" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d971cc77a245ccf1756dbd1a87c3e7f709c0191464096510d43eec056d0f2c4f" +dependencies = [ + "ahash", + "bumpalo", + "bytes", + "cfg-if", + "faststr", + "itoa", + "ref-cast", + "serde", + "simdutf8", + "sonic-number", + "sonic-simd", + "thiserror 2.0.18", + "zmij", +] + +[[package]] +name = "sonic-simd" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f99e664ecd2d85a68c87e3c7a3cfe691f647ea9e835de984aba4d54a41f817d4" +dependencies = [ + "cfg-if", ] [[package]] @@ -4094,7 +4280,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -4197,9 +4383,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -4243,9 +4429,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.50.0" +version = "1.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c" dependencies = [ "bytes", "libc", @@ -4260,9 +4446,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.1" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", @@ -4407,13 +4593,18 @@ version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" dependencies = [ + "async-compression", "bitflags", "bytes", + "futures-core", "futures-util", "http 1.4.0", "http-body 1.0.1", + "http-body-util", "iri-string", "pin-project-lite", + "tokio", + "tokio-util", "tower", "tower-layer", "tower-service", @@ -4662,9 +4853,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.115" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6523d69017b7633e396a89c5efab138161ed5aafcbc8d3e5c5a42ae38f50495a" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" dependencies = [ "cfg-if", "once_cell", @@ -4675,9 +4866,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.65" +version = "0.4.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d1faf851e778dfa54db7cd438b70758eba9755cb47403f3496edd7c8fc212f0" +checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" dependencies = [ "js-sys", "wasm-bindgen", @@ -4685,9 +4876,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.115" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e3a6c758eb2f701ed3d052ff5737f5bfe6614326ea7f3bbac7156192dc32e67" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4695,9 +4886,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.115" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "921de2737904886b52bcbb237301552d05969a6f9c40d261eb0533c8b055fedf" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" dependencies = [ "bumpalo", "proc-macro2", @@ -4708,9 +4899,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.115" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a93e946af942b58934c604527337bad9ae33ba1d5c6900bbb41c2c07c2364a93" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" dependencies = [ "unicode-ident", ] @@ -4751,9 +4942,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.92" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84cde8507f4d7cfcb1185b8cb5890c494ffea65edbe1ba82cfd63661c805ed94" +checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" dependencies = [ "js-sys", "wasm-bindgen", @@ -4784,7 +4975,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] [[package]] @@ -5178,9 +5369,9 @@ dependencies = [ [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "x509-cert" @@ -5219,9 +5410,9 @@ checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -5230,9 +5421,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -5262,18 +5453,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", @@ -5303,9 +5494,9 @@ dependencies = [ [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -5314,9 +5505,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -5325,9 +5516,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 861c331..e34f618 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -22,13 +22,13 @@ license = "Apache-2.0" crate-type = ["cdylib", "rlib"] [dependencies] -adbc_core = "0.22.0" -adbc_ffi = "0.22.0" -sf_core = { git = "https://github.com/snowflakedb/universal-driver", subdirectory = "sf_core", rev = "42b799464edec27361668b6b18792c7cc38cb785" } -arrow-array = { version = "57.3.0", default-features = false, features = ["ffi", "chrono-tz"] } -arrow-buffer = { version = "57.3.0", default-features = false } -arrow-schema = { version = "57.3.0", default-features = false } -arrow-cast = { version = "57.3.0", default-features = false } +adbc_core = ">=0.22.0" +adbc_ffi = ">=0.22.0" +sf_core = { git = "https://github.com/snowflakedb/universal-driver", subdirectory = "sf_core", rev = "1b8fc700c6fc10464851407da90ac5f9645758f7" } +arrow-array = { version = "58.1.0", default-features = false, features = ["ffi", "chrono-tz"] } +arrow-buffer = { version = "58.1.0", default-features = false } +arrow-schema = { version = "58.1.0", default-features = false } +arrow-cast = { version = "58.1.0", default-features = false } log = "0.4.22" tokio = { version = "1", features = ["rt-multi-thread"] } percent-encoding = "2.3.2" diff --git a/rust/src/error.rs b/rust/src/error.rs index 98c6dc8..2e7587d 100644 --- a/rust/src/error.rs +++ b/rust/src/error.rs @@ -38,6 +38,8 @@ pub(crate) fn api_error_to_adbc_error(err: ApiError) -> Error { ApiError::ArrowParsing { .. } => Status::IO, ApiError::ChunkFetch { .. } => Status::IO, ApiError::Base64Decoding { .. } => Status::IO, + ApiError::HttpRequest { .. } => Status::IO, + ApiError::TokenRequest { .. } => Status::Unauthenticated, }; Error::with_message_and_status(err.to_string(), status) }