MarcusKainth commented on code in PR #4204:
URL: https://github.com/apache/iggy/pull/4204#discussion_r4058294791
##########
core/connectors/sinks/clickhouse_sink/src/body.rs:
##########
@@ -98,7 +98,10 @@ pub(crate) fn build_string_body(
let mut buf = Vec::with_capacity(messages.len() * 64);
for msg in messages {
match &msg.payload {
- Payload::Text(s) => {
+ // `Payload::Proto` holds proto text, so a passthrough body takes
+ // it the same way it takes `Payload::Text`. The JSON and RowBinary
+ // builders above cannot: they need a parsed document.
Review Comment:
I agree on the JSON builder, of all three this was the loudest. Both
builders now parse `Payload::Proto` and take the existing path on success. Text
that is not JSON is skipped, with an accompanying log line, so the existing CSV
and TSV passthrough cases from `raw_to_protobuf` are unaffected. One nuance:
`pretty_json` defaults to false in `proto_convert.rs` `L75`, so the default
fallback text is already one line. I have still gone with parse and
re-serialise as you suggested, since it also validates the document before
ClickHouse sees it.
One correction on RowBinary. It did not lose a failure signal.
`build_row_binary_body` on `master` is byte-identical to this branch, and it
never failed on a variant mismatch. On `master` the reparsed JSON reached
`serialize_row` and was inserted, or failed the batch only on a table mismatch.
The regression was data loss rather than a lost error, and the fix is the same
either way.
One item I want to flag which matches the behaviour on `master`. The string
passthrough writes text verbatim, so a pipeline configured with `pretty_json =
true` and `string_format = "json_each_row"` produces a multi-line JSON document
which ClickHouse will reject. A `text` stream carrying pretty JSON has the same
exposure on `master`; the `Payload::Proto` arm added in this PR makes it
reachable from a `proto_convert` pipeline. I have added documentation in the
`README.md` and left both arms as is, unless you'd rather I re-serialise
`Payload::Text`? I decided against this because it rewrites operator bytes on
master's path. Let me know if you want this resolved within this PR, and if so,
how.
Fixed in `30216eb`. Unit tests cover both builders and the string arm, and a
new `clickhouse/proto_text` integration test on the default JSONEachRow format
loads zero rows and counts zero errors on `d8a8ece`. The `master` vs `HEAD`
matrix is in the summary comment.
##########
core/connectors/sinks/doris_sink/src/lib.rs:
##########
@@ -1098,8 +1098,9 @@ impl Sink for DorisSink {
//
// The lone hard-abort is a non-JSON payload (via `?`): a stream-wide
// schema-contract violation, not a transient chunk failure. Under the
- // documented `schema = "json"` config the SDK drops non-JSON before
- // consume() is called, so this stands as a defensive guard.
+ // documented `schema = "json"` config the runtime's JSON decoder drops
+ // non-JSON bytes before consume() is called, so this stands as a
+ // defensive guard unless a format-converting transform is configured.
Review Comment:
I agree, the comment edit named the hole and then left it open. Doris now
parses proto text per chunk and keeps the abort for text that is not JSON, so
the schema-contract guard still fires for the `text_to_protobuf` and
`raw_to_protobuf` outputs in `proto_convert.rs` `L695` and `L714`. Delta takes
the same parse and keeps its batch error.
One correction on Iceberg. It is not the same shape as Doris and Delta.
`router/mod.rs` `L149-169` is a `filter_map` with a `warn!`, so it silently
dropped the non-JSON rows and only failed when every row was non-JSON, which is
worse, and there is a second site in `router/dynamic_router.rs` `L85-96` where
the route field returns `None`. Both now parse proto text. The silent-drop
shape itself predates this PR and I have left it as is.
Fixed in `30216eb`, with unit tests per sink and a `doris/proto_text`
integration test that loads zero rows on `d8a8ece`. Two things worth having on
record. No decoder produces `Payload::Proto` (`decoders/proto.rs` only emits
`Json` and `Raw`), so every one of these rows is transform-only exposure. And
`master` had its own silent loss on the same configuration once a descriptor is
set: `json_to_protobuf` returns binary `Payload::Raw` at `proto_convert.rs`
`L646`, the `json` tag sent it through `simd_json::to_owned_value`, and the SDK
dropped it before `consume()` ran (`sdk/src/sink.rs` `L203-214`). This branch
tags it `Raw` and delivers it. That row is in the matrix too.
##########
core/connectors/runtime/src/sink.rs:
##########
@@ -674,6 +680,10 @@ async fn process_messages(
continue;
};
+ // Read the tag off the payload before `try_into_vec` consumes it. The
+ // decoder's own schema names the format it reads, not the variant it
+ // returned, and a transform may have changed the variant since.
+ let schema = message.payload.schema();
Review Comment:
Correct on both. `flat_buffer` (`flatbuffer.rs` `L42`) and `proto`
(`proto.rs` `L48`) extract to JSON under the runtime's defaults the same way
`avro` does, so all three change tag, and the section now says so, including
that a `proto` stream into Doris improves. I have added the non-object trigger
as well: `encode_json_with_schema` returns `Err` for any top-level array or
scalar at `proto_convert.rs` `L285`, so a fully configured pipeline takes the
fallback for those messages too. The section is rewritten in the PR body.
##########
core/connectors/sinks/README.md:
##########
@@ -249,6 +249,8 @@ for message in messages {
While the schema of messages (that will be consumed from the Iggy stream),
cannot be controlled by the sink connector itself, the built-in configuration
allows to decide what's the expected format of the messages (the particular
`StreamDecoder` will be used).
+`messages_metadata.schema` names the variant each `Payload` in the batch
actually holds, which is not always the stream's `schema` setting. A decoder
may return a different form than the wire format it reads: with `schema =
"avro"` the Avro decoder extracts to JSON by default, so the batch arrives as
`Payload::Json`. A configured transform can change the variant again. Match on
the `Payload` itself and treat `messages_metadata.schema` as a description of
what arrived, not of how the stream was configured.
Review Comment:
Added. The paragraph now states three things. A poll can arrive as several
`consume()` calls, one per contiguous variant run, and every call repeats the
same `current_offset`. That offset is the partition's high-water offset from
the poll (`sink.rs` `L344`) and was batch-scoped on `master` too, so repeating
it per run is the correct contract rather than a new one, though a sink keying
output on it has to tolerate the repeat. And an empty batch keeps the stream's
configured schema. `e58b1cf`.
##########
core/connectors/sdk/src/sink.rs:
##########
@@ -196,7 +198,12 @@ impl<T: Sink + std::fmt::Debug> SinkContainer<T> {
}
};
- let payload = match
messages_metadata.schema.try_into_payload(message.payload) {
+ // The runtime tags each run from `Payload::schema`, so the
+ // tag names a variant here rather than a wire format.
+ let payload = match Payload::try_from_schema(
Review Comment:
I agree, and this is the finding I most wanted a second pair of eyes on. No
FFI signature changed and no variant was added, but the `Proto` discriminant
changed meaning. The skew is narrower than it first looks and worth stating
precisely: a plugin built against 0.4.0 under this runtime is better off on
`avro`, `flat_buffer` and `proto` streams, because it now receives a `json` tag
it can read, and worse off on exactly one pipeline, a `json` stream with
`proto_convert`, where it rebuilds the `proto`-tagged run as `Payload::Raw`
where `master` handed it `Payload::Json`. That one pipeline is enough to want
the boundary marked. Bumped to 0.5.0 in `1742d52`. The rebuild requirement is
stated in the PR body's Compatibility section, and the connector-sdk skill now
lists a `Schema` variant that changes meaning alongside a new variant as a
breaking change.
On the parse, it is real and now documented, but it comes from the runtime
tag rather than the SDK swap. The `Json` arm of `Payload::try_from_schema` is
character-identical to `Schema::try_into_payload`'s (`lib.rs` `L189-191` and
`L296-298`); the arm that runs changed because the tag did. On `master` the
runtime paid `simd_json::to_vec` and then handed the plugin a `Payload::Avro`
holding JSON bytes it could not use, so the cost was only avoided by being
broken.
##########
core/connectors/sdk/README.md:
##########
@@ -175,7 +177,7 @@ These are SDK configuration fields, not Random or Stdout
`plugin_config` keys. T
#### Transform Options
- **`proto_convert`**: Transform for converting between protobuf and other
formats
-- **`source_format`** / **`target_format`**: Formats to convert between - any
schema value (`json`, `raw`, `text`, `proto`, `flat_buffer`, `avro`)
+- **`source_format`** / **`target_format`**: Formats to convert between - any
schema value (`json`, `raw`, `text`, `proto`, `flat_buffer`, `avro`).
`source_format` must match the variant the decoder or a preceding transform
actually produced, or the message is rejected
Review Comment:
Both right. Only `flatbuffer_convert` has the guard (`flatbuffer_convert.rs`
`L114`), and the README's own example at `L128` relies on the absence of one in
`proto_convert`. I have limited the sentence to `flatbuffer_convert` and added
a line that chain order is not defined, because `TransformsConfig` is a
`HashMap` (`configs/connectors.rs` `L250-254`). Changing that to an ordered map
is a separate behaviour change and I think it deserves its own issue rather
than folding it in here. Happy to raise it if you agree. `e58b1cf`.
##########
core/connectors/sinks/elasticsearch_sink/src/lib.rs:
##########
@@ -381,7 +381,9 @@ impl Sink for ElasticsearchSink {
}
}
}
- Payload::Text(text) => simd_json::json!({
+ // `Payload::Proto` holds proto text, so it indexes the
+ // same way `Payload::Text` does.
+ Payload::Text(text) | Payload::Proto(text) =>
simd_json::json!({
Review Comment:
I agree, and the arm was also inconsistent with its `Raw` sibling three
lines up, which already parses first (`elasticsearch_sink/src/lib.rs`
`L372-383`). Both sinks now try the parse and only fall back to the text arm
for proto text that is not JSON. Quickwit had the same flattening before this
PR (`quickwit_sink/src/lib.rs` `L346-349`), so I have fixed it in its own
commit, `c858418`, for consistency. Easy to drop if you would rather keep this
PR narrower. `30216eb` for the two you raised.
##########
core/connectors/sinks/meilisearch_sink/src/lib.rs:
##########
@@ -360,7 +360,9 @@ impl MeilisearchSink {
]),
}
}
- Payload::Text(text) => Map::from_iter([
+ // `Payload::Proto` holds proto text, so it indexes the same
+ // way `Payload::Text` does.
+ Payload::Text(text) | Payload::Proto(text) => Map::from_iter([
Review Comment:
Same as the Elasticsearch arm, fixed in the same commit (`30216eb`): the
parse runs first and the text arm is only the fallback for proto text that is
not JSON.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]