Skip to content

[connectors] Add preprocessor API with ConnectorMetadata#6580

Merged
mihaibudiu merged 1 commit into
feldera:mainfrom
mihaibudiu:issue6578
Jul 9, 2026
Merged

[connectors] Add preprocessor API with ConnectorMetadata#6580
mihaibudiu merged 1 commit into
feldera:mainfrom
mihaibudiu:issue6578

Conversation

@mihaibudiu

@mihaibudiu mihaibudiu commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Fixes #6578

Two commits:

  • pass ConnectorMetadata by reference to parsers. I think this was an API design bug
  • implement new preprocessor method

Adds a new method to the Preprocessor::process_with_metadata trait which receives the connector metadata as well.
This change should be backwards-compatible.

Checklist

  • Unit tests added/updated
  • Documentation updated

@mythical-fred mythical-fred left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Comment thread docs.feldera.com/docs/sql/udf.md Outdated
&mut self,
data: &[u8],
metadata: Option<ConnectorMetadata>,
) -> (Vec<u8>, Vec<ParseError>);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: showing process_with_metadata with a bare ; makes it look like a required method. Since it has a default impl that delegates to process, consider showing the body so users know they only need to override it when they actually want the metadata:

fn process_with_metadata(
    &mut self,
    data: &[u8],
    metadata: Option<ConnectorMetadata>,
) -> (Vec<u8>, Vec<ParseError>) {
    self.process(data)
}

///
/// # Arguments
/// * `data` - Raw input data bytes
/// * `_metadata` - Connector metadata

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: the doc comment says _metadata — drop the underscore prefix in documentation (it's an implementation detail for suppressing the unused-variable warning in the default impl).

@mythical-fred mythical-fred left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Backwards-compatible extension done the right way — new process_with_metadata with a default that delegates to process, so no existing impl breaks. Tests exercise both the "metadata-aware" and "no metadata" paths through the trait's public conversion into Variant, which is the right seam for user-defined preprocessors.

A couple of small things worth considering:

  1. metadata.clone() per chunk in the streaming path. In StreamingPreprocessedParser::parse, metadata is cloned once for process_with_metadata and then again inside the while let Some(chunk) loop for every parser call. ConnectorMetadata is a BTreeMap<Variant, Variant>, and Variant clones aren't free (some variants carry Arcs, others own their bytes). For high-throughput sources with many attributes this becomes measurable. The typical fix is to hand the parser an &Option<ConnectorMetadata> (or wrap the metadata in Arc at the top of the pipeline) so the leaves clone only when they actually need to keep it. Not blocking for this PR, but worth a follow-up.

  2. The trait in udf.md is now shown without the default body, which reads as "you must implement process_with_metadata". Since the whole point of the new signature is that it defaults to process, the doc snippet should either show the default ({ self.process(data) }) or add a one-liner immediately below the code block saying so. Otherwise UDF authors will wonder why the compiler doesn't complain when they omit it.

  3. Minor: the doc paragraph If the connector does not provide metadata, its value is None. — recommend `None` in code font, and the sentence just above says "an optional argument" — you can drop one of them.

The preprocesspreprocessor field-name doc fix is a nice catch, thanks.

@mihaibudiu
mihaibudiu requested a review from snkas July 3, 2026 16:16

@snkas snkas left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand correctly, it's backward compatible in that there is a default implementation for the process_with_metadata variant that drops the metadata?

What's the difference between process(data) and process_with_metadata(data, None)? If a user implements both, then the former is never used?

Could you add an end-to-end integration test for it?

Comment thread crates/adapterlib/src/preprocess.rs Outdated

impl Preprocessor for TopicPrefixPreprocessor {
fn process(&mut self, data: &[u8]) -> (Vec<u8>, Vec<ParseError>) {
(data.to_vec(), vec![])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems backward, in that shouldn't process call process_with_metadata(data, None) rather than the other way around?

@mihaibudiu mihaibudiu Jul 3, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did it this way for backwards compatibility - since we have users who have already implemented process but didn't know about the other method.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apparently it's possible to have two default implementations which call each other, so maybe I will do that instead.

Comment thread crates/adapterlib/src/format.rs Outdated
let (pre_data, mut pre_errors) = self.preprocessor.process(data);
let (pre_data, mut pre_errors) = self
.preprocessor
.process_with_metadata(data, metadata.clone());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is metadata passed by value instead of an immutable ref?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a design mistake in the original parser API, I will submit a commit to change this.

@mihaibudiu

Copy link
Copy Markdown
Contributor Author

I have force-pushed 2 commits.

@mythical-fred mythical-fred left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rebase LGTM. Arc-based sharing is correct: Arc::make_mut gives copy-on-write semantics on insert, and the two From<...> for Variant impls correctly distinguish the owned (move) and borrowed (cheap Arc::clone) paths. Two tiny nits inline; none blocking.

the pipeline with a overflow.

:::

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: "crash the pipeline with a overflow" -> "with a stack overflow" (or "with an infinite recursion"). The Rust source doc comment on process_with_metadata already says "stack overflow" correctly; docs should match.

/// # Arguments
/// * `data` - Raw input data bytes
/// * `_metadata` - Connector metadata
///

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit (carried from prior review): the Arguments doc still names the param _metadata. The underscore is only there to silence the unused-var warning in the default body; readers of the rustdoc shouldn't see it. Prefer * metadata - Connector metadata. Cosmetic.

@mihaibudiu
mihaibudiu requested a review from snkas July 3, 2026 20:13
/// to populate some of the table columns.
#[derive(Default, Clone, Debug, PartialEq, Eq)]
pub struct ConnectorMetadata(BTreeMap<Variant, Variant>);
pub struct ConnectorMetadata(Arc<BTreeMap<Variant, Variant>>);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realize this is pre-existing but why are we using our sql types here in something unrelated to sql types

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wondered the same, these should probably be just strings. @ryzhyk do you know?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Connector metadata exists so we can put it in SQL tables. Which is why we use SQL types.

@snkas snkas left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the preprocessor only used if the trait is implemented? As the default implementation would cause a stack overflow.

Comment thread docs.feldera.com/docs/sql/udf.md Outdated

@mythical-fred mythical-fred left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The added process_with_metadata API and its tests look good — the default-forwarding pair is a clean way to keep the trait backward-compatible, and the TopicPrefixPreprocessor test exercises the metadata path end-to-end through the public Variant conversion. Small doc nit inline; my prior APPROVE stands.

Comment thread docs.feldera.com/docs/sql/udf.md Outdated

A preprocessor that implements neither processing method compiles
without warnings, but the two default implementations will crash
the pipeline with a overflow.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo — should be a stack overflow, matching the trait docstring in preprocess.rs (which says "panic at runtime with a stack overflow"). Consistency helps readers who bounce between docs and source.

@mihaibudiu
mihaibudiu force-pushed the issue6578 branch 2 times, most recently from 088acd0 to e655a1a Compare July 9, 2026 20:55
Signed-off-by: Mihai Budiu <mbudiu@feldera.com>
@mihaibudiu
mihaibudiu enabled auto-merge July 9, 2026 20:57
@mihaibudiu
mihaibudiu added this pull request to the merge queue Jul 9, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 9, 2026
@mihaibudiu
mihaibudiu added this pull request to the merge queue Jul 9, 2026

@mythical-fred mythical-fred left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice follow-up on tip 04ad9018:

  • ConnectorMetadata gains a proper public read API (get, get_by_name) with an insert/get key-encoding invariant test — much cleaner than round-tripping through Variant::from(metadata.clone()) and pattern-matching on the resulting Variant::Map.
  • The topic typo I flagged ("a overflow" → "a stack overflow") is fixed in the trait doc.
  • Docs updated to teach get_by_name as the common lookup, with a copy-pasteable Kafka-topic example.
  • Test coverage for both getters and for the metadata-carrying preprocessor path.

APPROVE stands.

Merged via the queue into feldera:main with commit 6371a13 Jul 9, 2026
1 check passed
@mihaibudiu
mihaibudiu deleted the issue6578 branch July 9, 2026 22:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[connectors] Expose connector metadata to preprocessors

5 participants