Skip to content

KAFKA-20220: Enable TimestampedKVStoreWithHeaders in DSL (1/N)#21572

Merged
frankvicky merged 28 commits intoapache:trunkfrom
aliehsaeedii:dsl_with_headers
Feb 27, 2026
Merged

KAFKA-20220: Enable TimestampedKVStoreWithHeaders in DSL (1/N)#21572
frankvicky merged 28 commits intoapache:trunkfrom
aliehsaeedii:dsl_with_headers

Conversation

@aliehsaeedii
Copy link
Contributor

@aliehsaeedii aliehsaeedii commented Feb 24, 2026

This PR introduces the DslStoreFormat enum and extends DslKeyValueParams
to enable headers-aware key-value stores in the Kafka Streams DSL,
implementing the foundational infrastructure for
KIP-1285
.

Reviewers: Matthias J. Sax matthias@confluent.io, TengYao Chi
frankvicky@apache.org

@github-actions github-actions bot added triage PRs from the community streams labels Feb 24, 2026
@aliehsaeedii aliehsaeedii changed the title /KAFKA-20220: Enable TimestampedKVStoreWithHeaders in DSL (1/N) KAFKA-20220: Enable TimestampedKVStoreWithHeaders in DSL (1/N) Feb 25, 2026
Copy link
Contributor

@frankvicky frankvicky left a comment

Choose a reason for hiding this comment

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

Thanks for the PR

Comment on lines +297 to +312
@Test
public void shouldNotBuildHeadersAwareStoreWithCachingEvenIfExplicitlySet() {
doReturn("headers")
.when(streamsConfig).getString(StreamsConfig.DSL_STORE_FORMAT_CONFIG);

final MaterializedInternal<String, String, KeyValueStore<Bytes, byte[]>> materialized =
new MaterializedInternal<>(Materialized.<String, String, KeyValueStore<Bytes, byte[]>>as("store").withCachingEnabled(), nameProvider, STORE_PREFIX);

final TimestampedKeyValueStoreWithHeaders<String, String> store = getHeadersAwareStore(materialized);

final WrappedStateStore logging = (WrappedStateStore) ((WrappedStateStore) store).wrapped();
assertThat(store, instanceOf(MeteredTimestampedKeyValueStoreWithHeaders.class));
assertThat(logging, instanceOf(ChangeLoggingTimestampedKeyValueBytesStoreWithHeaders.class));
// Verify that caching layer was NOT added
assertThat(logging, not(instanceOf(CachingKeyValueStore.class)));
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks @frankvicky. Fixed the test. The code is correct but the test does not make sense.

@github-actions github-actions bot removed the triage PRs from the community label Feb 27, 2026
Copy link
Member

@mjsax mjsax left a comment

Choose a reason for hiding this comment

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

Made a pass (skipped tests for now)

config.originals()
);
}
if (dslStoreFormat == null) {
Copy link
Member

Choose a reason for hiding this comment

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

We did not update the constructor to pass in DslStoreFormat, so it would always be null here atm. Is this something that will change with follow up PRs? Or would we never need to pass via constructor, and this check is not necessary as we would always set it from the config?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes we must always set it from the confg. So i remove the if clause.

}
if (dslStoreFormat == null) {
final String dslStoreFormatValue = config.getString(StreamsConfig.DSL_STORE_FORMAT_CONFIG);
if (dslStoreFormatValue.equalsIgnoreCase("HEADERS")) {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if (dslStoreFormatValue.equalsIgnoreCase("HEADERS")) {
if (dslStoreFormatValue.equalsIgnoreCase(StreamsConfig.DSL_STORE_FORMAT_HEADERS)) {

public static final String DSL_STORE_FORMAT_DEFAULT = "DEFAULT";
private static final String DSL_STORE_FORMAT_DOC = "Controls the state store type used by the DSL store supplier (see " +
"config 'dsl.store.suppliers.class'). " +
"'default' uses timestamped stores. " +
Copy link
Member

Choose a reason for hiding this comment

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

This is not totally correct, right? Or do we want to just ignore this fact? Might be ok as-is; just double checking.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I changed it to

private static final String DSL_STORE_FORMAT_DOC = "Specifies the state store format for DSL operators. " +
        "'DEFAULT' creates either timestamped or plain state stores, depending on context. " +
        "'HEADERS' creates headers-aware stores that preserve record headers.";

This is more accurate.

public static final String DSL_STORE_FORMAT_CONFIG = "dsl.store.format";
public static final String DSL_STORE_FORMAT_DEFAULT = "DEFAULT";
private static final String DSL_STORE_FORMAT_DOC = "Controls the state store type used by the DSL store supplier (see " +
"config 'dsl.store.suppliers.class'). " +
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
"config 'dsl.store.suppliers.class'). " +
"config '" + DSL_STORE_SUPPLIER_CONFIG + "'). " +

? Stores.persistentTimestampedKeyValueStore(params.name())
: Stores.persistentKeyValueStore(params.name());
final DslStoreFormat storeFormat = params.dslStoreFormat();
if (storeFormat.equals(DslStoreFormat.HEADERS)) {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if (storeFormat.equals(DslStoreFormat.HEADERS)) {
if (storeFormat == DslStoreFormat.HEADERS) {

Copy link
Member

Choose a reason for hiding this comment

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

Or maybe even use switch (storeFormat) ?

if (dslStoreFormatValue.equalsIgnoreCase("HEADERS")) {
dslStoreFormat = DslStoreFormat.HEADERS;
} else { // DEFAULT
dslStoreFormat = DslStoreFormat.TIMESTAMPED;
Copy link
Member

Choose a reason for hiding this comment

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

This logic is too generic -- it works for most DSL operators but some need to translate "DEFAULT" to PLAIN.

I don't think we can make this translation here, but need to push it down a lower layer where it's know how to translate it?

materialized.keySerde(),
materialized.valueSerde());
} else {
} else if (storeFormat.equals(DslStoreFormat.HEADERS)) {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
} else if (storeFormat.equals(DslStoreFormat.HEADERS)) {
} else if (storeFormat == DslStoreFormat.HEADERS) {

Copy link
Member

Choose a reason for hiding this comment

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

Similar elsewhere. Enums are not object; we can use ==

supplier,
materialized.keySerde(),
materialized.valueSerde());
} else if (storeFormat.equals(DslStoreFormat.PLAIN)) {
Copy link
Member

Choose a reason for hiding this comment

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

Given the current code-setup, dslStoreFormat() would never return PLAIN, right? So this case is currently not possible?

final LeftOrRightValueSerde<V1, V2> leftOrRightValueSerde = new LeftOrRightValueSerde<>(streamJoined.valueSerde(), streamJoined.otherValueSerde());

final DslKeyValueParams dslKeyValueParams = new DslKeyValueParams(name, false);
final DslStoreFormat storeFormat = dslStoreFormat() == DslStoreFormat.HEADERS ? dslStoreFormat() : DslStoreFormat.PLAIN;
Copy link
Member

Choose a reason for hiding this comment

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

Looks not clean to me, to override TIMESTAMPED as PLAIN here?

* @param dslStoreFormat the format of the state store, see ({@link DslStoreFormat}
*/
public DslKeyValueParams(final String name, final DslStoreFormat dslStoreFormat) {
Objects.requireNonNull(name);
Copy link
Contributor

Choose a reason for hiding this comment

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

Duplicate check

* @param name the name of the store (cannot be {@code null})
* @param isTimestamped whether the returned stores should be timestamped, see ({@link TimestampedKeyValueStore}
*/
@Deprecated
Copy link
Contributor

Choose a reason for hiding this comment

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

@aliehsaeedii do you want to keep it for now?

@aliehsaeedii
Copy link
Contributor Author

@frankvicky

@aliehsaeedii do you want to keep it for now?

we keep the constructor but we deprecate it as it is listed in the KIP (kip-1285).

@frankvicky frankvicky merged commit 9f22628 into apache:trunk Feb 27, 2026
23 checks passed
if (dslStoreFormatValue.equalsIgnoreCase(StreamsConfig.DSL_STORE_FORMAT_HEADERS)) {
dslStoreFormat = DslStoreFormat.HEADERS;
}
// else dslStoreFormat remains null and the lower layers decide between PLAIN and TIMESTAMPED
Copy link
Member

@mjsax mjsax Feb 28, 2026

Choose a reason for hiding this comment

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

I actually proposed, that we would pass dslStoreFormatValue down to lower layers, and let them translated the String config from dsl.store.format to the DslStoreFormat enum...

But I guess this also work. Feel a little bit less clean to me personally, but maybe not worth to change now that this PR is already merged.

@mjsax mjsax added the kip Requires or implements a KIP label Mar 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci-approved kip Requires or implements a KIP streams

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants