Add Snowflake snapshot input connector#6601
Conversation
mythical-fred
left a comment
There was a problem hiding this comment.
Draft, so high-level only. Architecturally clean: new feldera-snowflake crate under with-snowflake in the default feature set, SnowflakeReaderConfig mirrored in Rust and Java with parallel validators and matching test coverage, integrated-connector plumbing wired through ConnectorValidator / integrated.rs / TransportConfig::SnowflakeInput. Unit tests for query construction, config validation, Arrow decoding, gzip streaming, URL redaction, and an #[ignore]d live smoke test. Nice shape overall.
Higher-level things worth deciding before this comes out of draft:
-
Snapshot-only, but framed as extensible.
SnowflakeIngestModeandSnowflakeTransactionModeare single-variant enums (Snapshot/None+Snapshot). That is fine as forward-compatible wire config, but the roadmap should be explicit: is CDC viaCHANGES/ streams / Snowpipe the intended follow-up, or is this permanently a bulk-snapshot connector? Thefault_tolerance() -> NoneandReplay { .. } => panic!decisions hinge on that answer — for a snapshot-only connector they are reasonable; for a future CDC mode they will need to be revisited before merge, not after. -
commit_transaction_on_errorsemantics. On mid-stream failure withtransaction_mode: snapshotthe connector currently publishes acommit_transactionfor whatever it already queued. The inline comment acknowledges the constraint ("Feldera does not currently expose transaction rollback to connectors"), but the resulting behaviour — surface a partial snapshot as a committed Feldera transaction — is the opposite of whatsnapshottransaction mode should promise. Worth discussing whether the correct default is to signal an unhealthy connector + refuse to commit, or to wait for a rollback primitive in the pipeline before shipping thesnapshotvariant oftransaction_modeat all. -
max_concurrent_readershas no upper bound.#[schema(minimum = 1)]+ runtime check for> 0, but a user can request 10_000.flatten_unordered(max_concurrent_readers)will happily open that many concurrent HTTP streams against Snowflake's chunk storage. Cap it (say, 64) or document a hard limit; do not leave it as a foot-gun. -
Two crypto stacks.
jsonwebtokenin the workspace usesaws-lc-rs, but the new crate pulls inopenssljust to load the RSA private key and compute a public-key SHA-256 fingerprint. That works, but it doubles the crypto surface in a connector that only signs a JWT. Consider usingrsa+sha2(pure Rust) oraws-lc-rs's own key-parsing APIs so the whole connector rides on one crypto backend. -
client.rsis 650 lines and covers auth, JWT signing, session management, statement execution, chunk streaming, retry policy, and URL redaction. Not a blocker, but splitting intoauth.rs(JWT + login) andquery.rs(statement + chunks) before it grows a CDC path will keep review-ability sane. -
Replaypanics. Even for snapshot-only that is a hard crash on a legitimate control-plane message. Prefer returning anerror(true, ...)on the consumer and letting the controller unwind rather than tearing the process down.
For ready-to-review: add docs.feldera.com/docs/connectors/sources/snowflake.md, changelog entry, and a manual test plan describing the credentials/warehouse used for the #[ignore] smoke test.
| return ok; | ||
| } | ||
|
|
||
| private boolean checkOptionalNonEmpty(ConfigReporter reporter, @Nullable String value, String name) { |
There was a problem hiding this comment.
I would move this function into the base interface, could be useful for other users.
| use anyhow::{bail, Result as AnyResult}; | ||
| use feldera_types::program_schema::Relation; | ||
|
|
||
| pub(crate) fn build_snapshot_query( |
There was a problem hiding this comment.
Is this function identical to the one for Delta?
If so, we should pull it into a common library build_sql_snapshot_query
| fn skips_unused_columns_when_requested_by_table() { | ||
| let mut relation = relation(&["ID", "UNUSED"]); | ||
| relation.fields[1].unused = true; | ||
| let position = SourcePosition { |
There was a problem hiding this comment.
don't we have a default() method on SourcePosition? Or perhaps an INVALID singleton?
We should use that.
| /// Key-pair authentication using a JWT signed with an RSA private key. | ||
| #[default] | ||
| #[serde(rename = "SNOWFLAKE_JWT")] | ||
| SnowflakeJwt, |
There was a problem hiding this comment.
I assume this is designed to grow
| /// | ||
| /// The value can be a fully-qualified Snowflake table name, for example | ||
| /// `"MY_DATABASE.MY_SCHEMA.MY_TABLE"`, or an unqualified table name when | ||
| /// `database` and `schema` are configured on the Snowflake session. |
There was a problem hiding this comment.
What if both specify a schema?
| pub(crate) struct SnowflakeClient { | ||
| pub(super) http: Client, | ||
| pub(super) base_url: Url, | ||
| session_token: String, |
There was a problem hiding this comment.
I wonder why some are pub and this one isn't
| for (field, column) in schema.fields().iter().zip(batch.columns()) { | ||
| let logical_type = field.metadata().get("logicalType").map(String::as_str); | ||
|
|
||
| // Snowflake encodes scaled FIXED values as unscaled signed integers. Its C/C++ client |
There was a problem hiding this comment.
I would move this comment to the normalize_fixed function
| // Snowflake's reference implementation documents both the current structured timestamp | ||
| // encoding and the legacy TIMESTAMP_TZ layout handled below: | ||
| // https://github.com/snowflakedb/libsnowflakeclient/blob/master/cpp/lib/ArrowChunkIterator.cpp | ||
| if let Some(values) = column.as_any().downcast_ref::<StructArray>() { |
There was a problem hiding this comment.
I hope this is right, I haven't checked, having some tests with real data will confirm
| anyhow!("Snowflake timestamp value in column '{field_name}' at row {row} is out of range") | ||
| } | ||
|
|
||
| fn struct_child<'a, T: Array + 'static>( |
There was a problem hiding this comment.
This function is only used for timestamp decoding, the name should reflect that, and perhaps some documentation too.
The errors produced are otherwise very mysterious.
| pub(crate) batches: BoxStream<'a, AnyResult<RecordBatch>>, | ||
| } | ||
|
|
||
| pub(crate) fn build_snapshot_query( |
There was a problem hiding this comment.
Also, pub functions should be documented.
|
To accept this we probably would need some end-to-end integration tests that generate data and load it. I wonder whether you need a real snowflake installation, or there's a tool to mock one. |
|
while reviewing, i see lot of this is something that can be abstracted away if we use some already tested library like https://github.com/andrusha/snowflake-rs ( or some other libraries? ) have we tried using it? if yes, why we decided to roll our own implementation? any downside to snowflake-rs? can we fork & solve it instead if it exists? |
i did take a look at the existing libraries, but it didn't support streaming semantics for its query api. since the only thing i cared about was a performant query api and the surface area was small, i ended up implementing from scratch. to be honest, i didn't think too hard about this and wasn't sure if we wanted to maintain a fork. i'm totally fine with forking and adapting it to support (thinking about it more, it would have been nice to atleast reuse the auth) |
agreed. i know of this tool, but it's pretty basic and doesn't emulate snowflake's query api exactly. we will probably want to run tests against actual snowflake |
|
I don't think we can avoid having a real snowflake account and testing against that. I am ok with merging it though and adding tests later. Otherwise, this PR will bit rot. We just shouldn't list Snowflake among supported connector types. |
|
Merging this will also make sure that we have a release where the manager understands snowflake connector config. Let's create a meta-issue with a list of todos needed to productize this and merge once we're happy with what's already there.
|
maybe! I don't think current impl have session-token refresh, it would be good to have for client. |
Add an integrated Snowflake input connector backed by Arrow REST results. Support JWT key-pair authentication, projection pushdown, connector-managed snapshot transactions, concurrent chunk reads, streaming decode, and parallel parsing.
de807df to
337271d
Compare
Add an integrated Snowflake input connector backed by Arrow REST results. Support JWT key-pair authentication, projection pushdown, connector-managed snapshot transactions, concurrent chunk reads, streaming decode, and parallel parsing.
Describe Manual Test Plan
Checklist
Breaking Changes?
Mark if you think the answer is yes for any of these components:
Describe Incompatible Changes