mjsax commented on code in PR #14596:
URL: https://github.com/apache/kafka/pull/14596#discussion_r1373965049


##########
streams/src/main/java/org/apache/kafka/streams/query/VersionedKeyQuery.java:
##########
@@ -0,0 +1,77 @@
+/*
+ * 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 org.apache.kafka.streams.query;
+
+import java.time.Instant;
+import java.util.Objects;
+import java.util.Optional;
+import org.apache.kafka.common.annotation.InterfaceStability.Evolving;
+import org.apache.kafka.streams.state.VersionedRecord;
+
+/**
+ * Interactive query for retrieving a single record from a versioned state 
store based on its key and timestamp.

Review Comment:
   We should add `@param <K>` and `@parm <V>` to describe the generic types



##########
streams/src/main/java/org/apache/kafka/streams/query/VersionedKeyQuery.java:
##########
@@ -0,0 +1,77 @@
+/*
+ * 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 org.apache.kafka.streams.query;
+
+import java.time.Instant;
+import java.util.Objects;
+import java.util.Optional;
+import org.apache.kafka.common.annotation.InterfaceStability.Evolving;
+import org.apache.kafka.streams.state.VersionedRecord;
+
+/**
+ * Interactive query for retrieving a single record from a versioned state 
store based on its key and timestamp.
+ */
+@Evolving
+public final class VersionedKeyQuery<K, V> implements 
Query<VersionedRecord<V>> {
+
+    private final K key;
+    private final Optional<Instant> asOfTimestamp;
+
+    private VersionedKeyQuery(final K key, final Optional<Instant> 
asOfTimestamp) {
+        this.key = Objects.requireNonNull(key);
+        this.asOfTimestamp = asOfTimestamp;
+    }
+
+    /**
+     * Creates a query that will retrieve the record from a versioned state 
store identified by {@code key} if it exists
+     * (or {@code null} otherwise).
+     * @param key The key to retrieve
+     * @param <K> The type of the key
+     * @param <V> The type of the value that will be retrieved
+     * @throws NullPointerException if @param key is null
+     */
+    public static <K, V> VersionedKeyQuery<K, V> withKey(final K key) {
+        return new VersionedKeyQuery<>(key, Optional.empty());
+    }
+
+    /**
+     * Specifies the timestamp for the key query. The key query returns the 
record version for the specified timestamp.

Review Comment:
   `record's` ? (not sure)



##########
streams/src/main/java/org/apache/kafka/streams/query/VersionedKeyQuery.java:
##########
@@ -0,0 +1,77 @@
+/*
+ * 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 org.apache.kafka.streams.query;
+
+import java.time.Instant;
+import java.util.Objects;
+import java.util.Optional;
+import org.apache.kafka.common.annotation.InterfaceStability.Evolving;
+import org.apache.kafka.streams.state.VersionedRecord;
+
+/**
+ * Interactive query for retrieving a single record from a versioned state 
store based on its key and timestamp.
+ */
+@Evolving
+public final class VersionedKeyQuery<K, V> implements 
Query<VersionedRecord<V>> {
+
+    private final K key;
+    private final Optional<Instant> asOfTimestamp;
+
+    private VersionedKeyQuery(final K key, final Optional<Instant> 
asOfTimestamp) {
+        this.key = Objects.requireNonNull(key);
+        this.asOfTimestamp = asOfTimestamp;
+    }
+
+    /**
+     * Creates a query that will retrieve the record from a versioned state 
store identified by {@code key} if it exists
+     * (or {@code null} otherwise).

Review Comment:
   Should we explain that by default we query for the "latest" version of the 
key, and refer to `asOf` as further explanation? 



##########
streams/src/main/java/org/apache/kafka/streams/query/VersionedKeyQuery.java:
##########
@@ -0,0 +1,77 @@
+/*
+ * 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 org.apache.kafka.streams.query;
+
+import java.time.Instant;
+import java.util.Objects;
+import java.util.Optional;
+import org.apache.kafka.common.annotation.InterfaceStability.Evolving;
+import org.apache.kafka.streams.state.VersionedRecord;
+
+/**
+ * Interactive query for retrieving a single record from a versioned state 
store based on its key and timestamp.
+ */
+@Evolving
+public final class VersionedKeyQuery<K, V> implements 
Query<VersionedRecord<V>> {
+
+    private final K key;
+    private final Optional<Instant> asOfTimestamp;
+
+    private VersionedKeyQuery(final K key, final Optional<Instant> 
asOfTimestamp) {
+        this.key = Objects.requireNonNull(key);
+        this.asOfTimestamp = asOfTimestamp;
+    }
+
+    /**
+     * Creates a query that will retrieve the record from a versioned state 
store identified by {@code key} if it exists
+     * (or {@code null} otherwise).
+     * @param key The key to retrieve
+     * @param <K> The type of the key
+     * @param <V> The type of the value that will be retrieved
+     * @throws NullPointerException if @param key is null
+     */
+    public static <K, V> VersionedKeyQuery<K, V> withKey(final K key) {
+        return new VersionedKeyQuery<>(key, Optional.empty());
+    }
+
+    /**
+     * Specifies the timestamp for the key query. The key query returns the 
record version for the specified timestamp.
+     * (To be more precise: The key query returns the record with the greatest 
timestamp <= asOfTimestamp)
+     * if @param asOfTimestamp is null, it will be considered as 
Optional.empty()
+     * @param asOfTimestamp The as of timestamp for timestamp
+     */
+    public VersionedKeyQuery<K, V> asOf(final Instant asOfTimestamp) {
+        if (asOfTimestamp == null) {
+            return new VersionedKeyQuery<>(key, Optional.empty());
+        }
+        return new VersionedKeyQuery<>(key, Optional.of(asOfTimestamp));
+    }
+
+    /**
+     * The key that was specified for this query.

Review Comment:
   We should add `@return` (even if somewhat redundant) for completeness (same 
for `asOfTimestamp` below).



##########
streams/src/main/java/org/apache/kafka/streams/state/internals/VersionedStoreQueryUtils.java:
##########
@@ -0,0 +1,189 @@
+/*
+ * 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 org.apache.kafka.streams.state.internals;
+
+import static org.apache.kafka.common.utils.Utils.mkEntry;
+import static org.apache.kafka.common.utils.Utils.mkMap;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.time.temporal.ChronoField;
+import java.util.Map;
+import org.apache.kafka.common.serialization.Deserializer;
+import org.apache.kafka.common.utils.Bytes;
+import org.apache.kafka.streams.processor.StateStore;
+import org.apache.kafka.streams.processor.StateStoreContext;
+import org.apache.kafka.streams.query.FailureReason;
+import org.apache.kafka.streams.query.Position;
+import org.apache.kafka.streams.query.PositionBound;
+import org.apache.kafka.streams.query.Query;
+import org.apache.kafka.streams.query.QueryConfig;
+import org.apache.kafka.streams.query.QueryResult;
+import org.apache.kafka.streams.query.VersionedKeyQuery;
+import org.apache.kafka.streams.state.StateSerdes;
+import org.apache.kafka.streams.state.VersionedKeyValueStore;
+import org.apache.kafka.streams.state.VersionedRecord;
+
+public final class VersionedStoreQueryUtils {
+
+    /**
+     * a utility interface to facilitate stores' query dispatch logic,
+     * allowing them to generically store query execution logic as the values
+     * in a map.
+     */
+    @FunctionalInterface
+    public interface QueryHandler {
+        QueryResult<?> apply(
+            final Query<?> query,
+            final PositionBound positionBound,
+            final QueryConfig config,
+            final StateStore store
+        );
+    }
+
+    @SuppressWarnings("rawtypes")
+    private static final Map<Class, QueryHandler> QUERY_HANDLER_MAP =
+        mkMap(
+            mkEntry(
+                VersionedKeyQuery.class,
+                VersionedStoreQueryUtils::runVersionedKeyQuery
+            )
+        );
+
+    // make this class uninstantiable
+
+    private VersionedStoreQueryUtils() {
+    }
+    @SuppressWarnings("unchecked")
+    public static <R> QueryResult<R> handleBasicQueries(
+        final Query<R> query,
+        final PositionBound positionBound,
+        final QueryConfig config,
+        final StateStore store,
+        final Position position,
+        final StateStoreContext context
+    ) {
+
+        final long start = config.isCollectExecutionInfo() ? System.nanoTime() 
: -1L;
+        final QueryResult<R> result;
+
+        final QueryHandler handler = QUERY_HANDLER_MAP.get(query.getClass());
+        if (handler == null) {
+            result = QueryResult.forUnknownQueryType(query, store);
+        } else if (context == null || !isPermitted(position, positionBound, 
context.taskId().partition())) {
+            result = QueryResult.notUpToBound(
+                position,
+                positionBound,
+                context == null ? null : context.taskId().partition()
+            );
+        } else {
+            result = (QueryResult<R>) handler.apply(
+                query,
+                positionBound,
+                config,
+                store
+            );
+        }
+        if (config.isCollectExecutionInfo()) {
+            result.addExecutionInfo(
+                "Handled in " + store.getClass() + " in " + (System.nanoTime() 
- start) + "ns"
+            );
+        }
+        result.setPosition(position);
+        return result;
+    }
+
+    public static boolean isPermitted(
+        final Position position,
+        final PositionBound positionBound,
+        final int partition
+    ) {
+        final Position bound = positionBound.position();
+        for (final String topic : bound.getTopics()) {
+            final Map<Integer, Long> partitionBounds = 
bound.getPartitionPositions(topic);
+            final Map<Integer, Long> seenPartitionPositions = 
position.getPartitionPositions(topic);
+            if (!partitionBounds.containsKey(partition)) {
+                // this topic isn't bounded for our partition, so just skip 
over it.
+            } else {
+                if (!seenPartitionPositions.containsKey(partition)) {
+                    // we haven't seen a partition that we have a bound for
+                    return false;
+                } else if (seenPartitionPositions.get(partition) < 
partitionBounds.get(partition)) {
+                    // our current position is behind the bound
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    @SuppressWarnings("unchecked")
+    private static <R> QueryResult<R> runVersionedKeyQuery(final Query<R> 
query,
+                                                  final PositionBound 
positionBound,
+                                                  final QueryConfig config,
+                                                  final StateStore store) {
+        if (store instanceof VersionedKeyValueStore) {
+            final VersionedKeyValueStore<Bytes, byte[]> versionedKeyValueStore 
=
+                (VersionedKeyValueStore<Bytes, byte[]>) store;
+            if (query instanceof VersionedKeyQuery) {
+                final VersionedKeyQuery<Bytes, byte[]> rawKeyQuery =
+                    (VersionedKeyQuery<Bytes, byte[]>) query;
+                try {
+                    final VersionedRecord<byte[]> bytes;
+                    if (((VersionedKeyQuery<?, ?>) 
query).asOfTimestamp().isPresent()) {
+                        bytes = versionedKeyValueStore.get(rawKeyQuery.key(),
+                            ((VersionedKeyQuery<?, ?>) 
query).asOfTimestamp().get()
+                                .getLong(ChronoField.INSTANT_SECONDS));

Review Comment:
   I think we need to use `toEpochMilli()` here to get the `Instant` converted 
to `long` correctly?



##########
streams/src/main/java/org/apache/kafka/streams/state/internals/VersionedStoreQueryUtils.java:
##########
@@ -0,0 +1,189 @@
+/*
+ * 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 org.apache.kafka.streams.state.internals;
+
+import static org.apache.kafka.common.utils.Utils.mkEntry;
+import static org.apache.kafka.common.utils.Utils.mkMap;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.time.temporal.ChronoField;
+import java.util.Map;
+import org.apache.kafka.common.serialization.Deserializer;
+import org.apache.kafka.common.utils.Bytes;
+import org.apache.kafka.streams.processor.StateStore;
+import org.apache.kafka.streams.processor.StateStoreContext;
+import org.apache.kafka.streams.query.FailureReason;
+import org.apache.kafka.streams.query.Position;
+import org.apache.kafka.streams.query.PositionBound;
+import org.apache.kafka.streams.query.Query;
+import org.apache.kafka.streams.query.QueryConfig;
+import org.apache.kafka.streams.query.QueryResult;
+import org.apache.kafka.streams.query.VersionedKeyQuery;
+import org.apache.kafka.streams.state.StateSerdes;
+import org.apache.kafka.streams.state.VersionedKeyValueStore;
+import org.apache.kafka.streams.state.VersionedRecord;
+
+public final class VersionedStoreQueryUtils {
+
+    /**
+     * a utility interface to facilitate stores' query dispatch logic,
+     * allowing them to generically store query execution logic as the values
+     * in a map.
+     */
+    @FunctionalInterface
+    public interface QueryHandler {
+        QueryResult<?> apply(
+            final Query<?> query,
+            final PositionBound positionBound,
+            final QueryConfig config,
+            final StateStore store
+        );
+    }
+
+    @SuppressWarnings("rawtypes")
+    private static final Map<Class, QueryHandler> QUERY_HANDLER_MAP =
+        mkMap(
+            mkEntry(
+                VersionedKeyQuery.class,
+                VersionedStoreQueryUtils::runVersionedKeyQuery
+            )
+        );
+
+    // make this class uninstantiable
+
+    private VersionedStoreQueryUtils() {
+    }
+    @SuppressWarnings("unchecked")
+    public static <R> QueryResult<R> handleBasicQueries(
+        final Query<R> query,
+        final PositionBound positionBound,
+        final QueryConfig config,
+        final StateStore store,
+        final Position position,
+        final StateStoreContext context
+    ) {
+
+        final long start = config.isCollectExecutionInfo() ? System.nanoTime() 
: -1L;
+        final QueryResult<R> result;
+
+        final QueryHandler handler = QUERY_HANDLER_MAP.get(query.getClass());
+        if (handler == null) {
+            result = QueryResult.forUnknownQueryType(query, store);
+        } else if (context == null || !isPermitted(position, positionBound, 
context.taskId().partition())) {
+            result = QueryResult.notUpToBound(
+                position,
+                positionBound,
+                context == null ? null : context.taskId().partition()
+            );
+        } else {
+            result = (QueryResult<R>) handler.apply(
+                query,
+                positionBound,
+                config,
+                store
+            );
+        }
+        if (config.isCollectExecutionInfo()) {
+            result.addExecutionInfo(
+                "Handled in " + store.getClass() + " in " + (System.nanoTime() 
- start) + "ns"
+            );
+        }
+        result.setPosition(position);
+        return result;
+    }
+
+    public static boolean isPermitted(
+        final Position position,
+        final PositionBound positionBound,
+        final int partition
+    ) {
+        final Position bound = positionBound.position();
+        for (final String topic : bound.getTopics()) {
+            final Map<Integer, Long> partitionBounds = 
bound.getPartitionPositions(topic);
+            final Map<Integer, Long> seenPartitionPositions = 
position.getPartitionPositions(topic);
+            if (!partitionBounds.containsKey(partition)) {
+                // this topic isn't bounded for our partition, so just skip 
over it.
+            } else {
+                if (!seenPartitionPositions.containsKey(partition)) {
+                    // we haven't seen a partition that we have a bound for
+                    return false;
+                } else if (seenPartitionPositions.get(partition) < 
partitionBounds.get(partition)) {
+                    // our current position is behind the bound
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    @SuppressWarnings("unchecked")
+    private static <R> QueryResult<R> runVersionedKeyQuery(final Query<R> 
query,
+                                                  final PositionBound 
positionBound,
+                                                  final QueryConfig config,
+                                                  final StateStore store) {
+        if (store instanceof VersionedKeyValueStore) {
+            final VersionedKeyValueStore<Bytes, byte[]> versionedKeyValueStore 
=
+                (VersionedKeyValueStore<Bytes, byte[]>) store;
+            if (query instanceof VersionedKeyQuery) {
+                final VersionedKeyQuery<Bytes, byte[]> rawKeyQuery =
+                    (VersionedKeyQuery<Bytes, byte[]>) query;
+                try {
+                    final VersionedRecord<byte[]> bytes;
+                    if (((VersionedKeyQuery<?, ?>) 
query).asOfTimestamp().isPresent()) {
+                        bytes = versionedKeyValueStore.get(rawKeyQuery.key(),
+                            ((VersionedKeyQuery<?, ?>) 
query).asOfTimestamp().get()
+                                .getLong(ChronoField.INSTANT_SECONDS));
+                    } else {
+                        bytes = versionedKeyValueStore.get(rawKeyQuery.key());
+                    }
+                    return (QueryResult<R>) QueryResult.forResult(bytes);
+                } catch (final Exception e) {
+                    final String message = parseStoreException(e, store, 
query);
+                    return QueryResult.forFailure(
+                        FailureReason.STORE_EXCEPTION,
+                        message
+                    );
+                }
+            } else {
+                // Here is preserved for VersionedMultiTimestampedKeyQuery
+                return QueryResult.forUnknownQueryType(query, store);
+            }
+        } else {
+            return QueryResult.forUnknownQueryType(query, store);
+        }
+    }
+
+    public static <V> VersionedRecord<V> deserializeVersionedRecord(final 
StateSerdes<?, V> serdes, final VersionedRecord<byte[]> rawVersionedRecord) {
+
+        final Deserializer<V> valueDeserializer = serdes.valueDeserializer();
+        final long timestamp = rawVersionedRecord.timestamp();
+        final V value = valueDeserializer.deserialize(serdes.topic(), 
rawVersionedRecord.value());
+        valueDeserializer.close();

Review Comment:
   `valueDesarializer()` does not create a new instance/object, but returns a 
shared object -- we should not close it.



##########
streams/src/main/java/org/apache/kafka/streams/state/internals/MeteredVersionedKeyValueStore.java:
##########
@@ -89,6 +102,18 @@ private class MeteredVersionedKeyValueStoreInternal
         private final Serde<V> plainValueSerde;
         private StateSerdes<K, V> plainValueSerdes;
 
+        private final Map<Class, QueryHandler> queryHandlers =
+            mkMap(
+                mkEntry(
+                    RangeQuery.class,

Review Comment:
   Ah I see -- but for this case, we also need to add `KeyQuery` and also keep 
the existing test.
   
   As mentioned elsewhere, we could add support for `KeyQuery` and `RangeQuery` 
in a follow up KIP, so for now we should keep the guard for both in place.



##########
streams/src/main/java/org/apache/kafka/streams/query/VersionedKeyQuery.java:
##########
@@ -0,0 +1,77 @@
+/*
+ * 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 org.apache.kafka.streams.query;
+
+import java.time.Instant;
+import java.util.Objects;
+import java.util.Optional;
+import org.apache.kafka.common.annotation.InterfaceStability.Evolving;
+import org.apache.kafka.streams.state.VersionedRecord;
+
+/**
+ * Interactive query for retrieving a single record from a versioned state 
store based on its key and timestamp.
+ */
+@Evolving
+public final class VersionedKeyQuery<K, V> implements 
Query<VersionedRecord<V>> {
+
+    private final K key;
+    private final Optional<Instant> asOfTimestamp;
+
+    private VersionedKeyQuery(final K key, final Optional<Instant> 
asOfTimestamp) {
+        this.key = Objects.requireNonNull(key);
+        this.asOfTimestamp = asOfTimestamp;
+    }
+
+    /**
+     * Creates a query that will retrieve the record from a versioned state 
store identified by {@code key} if it exists
+     * (or {@code null} otherwise).
+     * @param key The key to retrieve
+     * @param <K> The type of the key
+     * @param <V> The type of the value that will be retrieved
+     * @throws NullPointerException if @param key is null
+     */
+    public static <K, V> VersionedKeyQuery<K, V> withKey(final K key) {
+        return new VersionedKeyQuery<>(key, Optional.empty());
+    }
+
+    /**
+     * Specifies the timestamp for the key query. The key query returns the 
record version for the specified timestamp.
+     * (To be more precise: The key query returns the record with the greatest 
timestamp <= asOfTimestamp)

Review Comment:
   Is `<=` valid or does it need HTML escaping?



##########
streams/src/main/java/org/apache/kafka/streams/query/VersionedKeyQuery.java:
##########
@@ -0,0 +1,77 @@
+/*
+ * 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 org.apache.kafka.streams.query;
+
+import java.time.Instant;
+import java.util.Objects;
+import java.util.Optional;
+import org.apache.kafka.common.annotation.InterfaceStability.Evolving;
+import org.apache.kafka.streams.state.VersionedRecord;
+
+/**
+ * Interactive query for retrieving a single record from a versioned state 
store based on its key and timestamp.
+ */
+@Evolving
+public final class VersionedKeyQuery<K, V> implements 
Query<VersionedRecord<V>> {
+
+    private final K key;
+    private final Optional<Instant> asOfTimestamp;
+
+    private VersionedKeyQuery(final K key, final Optional<Instant> 
asOfTimestamp) {
+        this.key = Objects.requireNonNull(key);
+        this.asOfTimestamp = asOfTimestamp;
+    }
+
+    /**
+     * Creates a query that will retrieve the record from a versioned state 
store identified by {@code key} if it exists
+     * (or {@code null} otherwise).
+     * @param key The key to retrieve
+     * @param <K> The type of the key
+     * @param <V> The type of the value that will be retrieved
+     * @throws NullPointerException if @param key is null

Review Comment:
   Is `@param key` valid JavaDoc? -- we usually use `{@code key}` for stuff 
like this IIRC.



##########
streams/src/main/java/org/apache/kafka/streams/state/internals/VersionedStoreQueryUtils.java:
##########
@@ -0,0 +1,189 @@
+/*
+ * 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 org.apache.kafka.streams.state.internals;
+
+import static org.apache.kafka.common.utils.Utils.mkEntry;
+import static org.apache.kafka.common.utils.Utils.mkMap;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.time.temporal.ChronoField;
+import java.util.Map;
+import org.apache.kafka.common.serialization.Deserializer;
+import org.apache.kafka.common.utils.Bytes;
+import org.apache.kafka.streams.processor.StateStore;
+import org.apache.kafka.streams.processor.StateStoreContext;
+import org.apache.kafka.streams.query.FailureReason;
+import org.apache.kafka.streams.query.Position;
+import org.apache.kafka.streams.query.PositionBound;
+import org.apache.kafka.streams.query.Query;
+import org.apache.kafka.streams.query.QueryConfig;
+import org.apache.kafka.streams.query.QueryResult;
+import org.apache.kafka.streams.query.VersionedKeyQuery;
+import org.apache.kafka.streams.state.StateSerdes;
+import org.apache.kafka.streams.state.VersionedKeyValueStore;
+import org.apache.kafka.streams.state.VersionedRecord;
+
+public final class VersionedStoreQueryUtils {

Review Comment:
   Why do we need a new class? Can't we add what we need to existing 
`StoreQueryUtils` (seems we introduce a lot of (unnecessary?) code duplication)?



##########
streams/src/main/java/org/apache/kafka/streams/state/internals/MeteredVersionedKeyValueStore.java:
##########
@@ -148,13 +199,33 @@ protected <R> QueryResult<R> runRangeQuery(final Query<R> 
query,
             throw new UnsupportedOperationException("Versioned stores do not 
support RangeQuery queries at this time.");
         }
 
-        @Override
-        protected <R> QueryResult<R> runKeyQuery(final Query<R> query,
-                                                 final PositionBound 
positionBound,
-                                                 final QueryConfig config) {
-            // throw exception for now to reserve the ability to implement 
this in the future
-            // without clashing with users' custom implementations in the 
meantime
-            throw new UnsupportedOperationException("Versioned stores do not 
support KeyQuery queries at this time.");
+        @SuppressWarnings("unchecked")
+        protected <R> QueryResult<R> runVersionedKeyQuery(final Query<R> query,
+            final PositionBound positionBound,
+            final QueryConfig config) {

Review Comment:
   nit: fix indention



##########
streams/src/main/java/org/apache/kafka/streams/state/internals/MeteredVersionedKeyValueStore.java:
##########
@@ -148,13 +199,33 @@ protected <R> QueryResult<R> runRangeQuery(final Query<R> 
query,
             throw new UnsupportedOperationException("Versioned stores do not 
support RangeQuery queries at this time.");
         }
 
-        @Override
-        protected <R> QueryResult<R> runKeyQuery(final Query<R> query,
-                                                 final PositionBound 
positionBound,
-                                                 final QueryConfig config) {
-            // throw exception for now to reserve the ability to implement 
this in the future
-            // without clashing with users' custom implementations in the 
meantime
-            throw new UnsupportedOperationException("Versioned stores do not 
support KeyQuery queries at this time.");
+        @SuppressWarnings("unchecked")
+        protected <R> QueryResult<R> runVersionedKeyQuery(final Query<R> query,
+            final PositionBound positionBound,
+            final QueryConfig config) {
+            if (query instanceof VersionedKeyQuery) {
+                final QueryResult<R> result;
+                final VersionedKeyQuery<K, V> typedKeyQuery = 
(VersionedKeyQuery<K, V>) query;
+                VersionedKeyQuery<Bytes, byte[]> rawKeyQuery;
+                rawKeyQuery = 
VersionedKeyQuery.withKey(keyBytes(typedKeyQuery.key()));
+                if (typedKeyQuery.asOfTimestamp().isPresent()) {
+                    rawKeyQuery = 
rawKeyQuery.asOf(typedKeyQuery.asOfTimestamp().get());
+                }
+                final QueryResult<VersionedRecord<byte[]>> rawResult =
+                    wrapped().query(rawKeyQuery, positionBound, config);
+                if (rawResult.isSuccess()) {
+                    final VersionedRecord<V> versionedRecord = 
VersionedStoreQueryUtils.deserializeVersionedRecord(plainValueSerdes, 
rawResult.getResult());
+                    final QueryResult<VersionedRecord<V>> typedQueryResult =
+                        
InternalQueryResultUtil.copyAndSubstituteDeserializedResult(rawResult, 
versionedRecord);
+                    result = (QueryResult<R>) typedQueryResult;
+                } else {
+                    // the generic type doesn't matter, since failed queries 
have no result set.
+                    result = (QueryResult<R>) rawResult;
+                }
+                return result;
+            }
+            // reserved for other IQv2 query types (KIP-968, KIP-969)

Review Comment:
   Actually wondering if this is the right approach? If we add new query types, 
we would add new query handler and new methods, right? So we don't need the `if 
(query instanceof VersionedKeyQuery)` to begin with, because we would call 
`runVersionedKeyQuery` only if the type was `VerionedKeyQuery` to begin with 
(then query handler ensure this part)?



##########
streams/src/main/java/org/apache/kafka/streams/state/internals/MeteredVersionedKeyValueStore.java:
##########
@@ -148,13 +199,33 @@ protected <R> QueryResult<R> runRangeQuery(final Query<R> 
query,
             throw new UnsupportedOperationException("Versioned stores do not 
support RangeQuery queries at this time.");
         }
 
-        @Override
-        protected <R> QueryResult<R> runKeyQuery(final Query<R> query,
-                                                 final PositionBound 
positionBound,
-                                                 final QueryConfig config) {
-            // throw exception for now to reserve the ability to implement 
this in the future
-            // without clashing with users' custom implementations in the 
meantime
-            throw new UnsupportedOperationException("Versioned stores do not 
support KeyQuery queries at this time.");
+        @SuppressWarnings("unchecked")
+        protected <R> QueryResult<R> runVersionedKeyQuery(final Query<R> query,
+            final PositionBound positionBound,
+            final QueryConfig config) {
+            if (query instanceof VersionedKeyQuery) {
+                final QueryResult<R> result;
+                final VersionedKeyQuery<K, V> typedKeyQuery = 
(VersionedKeyQuery<K, V>) query;
+                VersionedKeyQuery<Bytes, byte[]> rawKeyQuery;
+                rawKeyQuery = 
VersionedKeyQuery.withKey(keyBytes(typedKeyQuery.key()));

Review Comment:
   Merge this and the line above?



##########
streams/src/main/java/org/apache/kafka/streams/query/VersionedKeyQuery.java:
##########
@@ -0,0 +1,77 @@
+/*
+ * 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 org.apache.kafka.streams.query;
+
+import java.time.Instant;
+import java.util.Objects;
+import java.util.Optional;
+import org.apache.kafka.common.annotation.InterfaceStability.Evolving;
+import org.apache.kafka.streams.state.VersionedRecord;
+
+/**
+ * Interactive query for retrieving a single record from a versioned state 
store based on its key and timestamp.
+ */
+@Evolving
+public final class VersionedKeyQuery<K, V> implements 
Query<VersionedRecord<V>> {
+
+    private final K key;
+    private final Optional<Instant> asOfTimestamp;
+
+    private VersionedKeyQuery(final K key, final Optional<Instant> 
asOfTimestamp) {
+        this.key = Objects.requireNonNull(key);
+        this.asOfTimestamp = asOfTimestamp;
+    }
+
+    /**
+     * Creates a query that will retrieve the record from a versioned state 
store identified by {@code key} if it exists
+     * (or {@code null} otherwise).
+     * @param key The key to retrieve
+     * @param <K> The type of the key
+     * @param <V> The type of the value that will be retrieved
+     * @throws NullPointerException if @param key is null
+     */
+    public static <K, V> VersionedKeyQuery<K, V> withKey(final K key) {
+        return new VersionedKeyQuery<>(key, Optional.empty());
+    }
+
+    /**
+     * Specifies the timestamp for the key query. The key query returns the 
record version for the specified timestamp.
+     * (To be more precise: The key query returns the record with the greatest 
timestamp <= asOfTimestamp)
+     * if @param asOfTimestamp is null, it will be considered as 
Optional.empty()

Review Comment:
   Did we discuss this in the KIP (cannot remember) -- by gut feeling would be 
that `null` should not be allowed (because not necessary, as "latest" is the 
default anyway)?



##########
streams/src/main/java/org/apache/kafka/streams/query/VersionedKeyQuery.java:
##########
@@ -0,0 +1,77 @@
+/*
+ * 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 org.apache.kafka.streams.query;
+
+import java.time.Instant;
+import java.util.Objects;
+import java.util.Optional;
+import org.apache.kafka.common.annotation.InterfaceStability.Evolving;
+import org.apache.kafka.streams.state.VersionedRecord;
+
+/**
+ * Interactive query for retrieving a single record from a versioned state 
store based on its key and timestamp.
+ */
+@Evolving
+public final class VersionedKeyQuery<K, V> implements 
Query<VersionedRecord<V>> {
+
+    private final K key;
+    private final Optional<Instant> asOfTimestamp;
+
+    private VersionedKeyQuery(final K key, final Optional<Instant> 
asOfTimestamp) {
+        this.key = Objects.requireNonNull(key);
+        this.asOfTimestamp = asOfTimestamp;
+    }
+
+    /**
+     * Creates a query that will retrieve the record from a versioned state 
store identified by {@code key} if it exists
+     * (or {@code null} otherwise).
+     * @param key The key to retrieve
+     * @param <K> The type of the key
+     * @param <V> The type of the value that will be retrieved
+     * @throws NullPointerException if @param key is null
+     */
+    public static <K, V> VersionedKeyQuery<K, V> withKey(final K key) {
+        return new VersionedKeyQuery<>(key, Optional.empty());
+    }
+
+    /**
+     * Specifies the timestamp for the key query. The key query returns the 
record version for the specified timestamp.
+     * (To be more precise: The key query returns the record with the greatest 
timestamp <= asOfTimestamp)
+     * if @param asOfTimestamp is null, it will be considered as 
Optional.empty()
+     * @param asOfTimestamp The as of timestamp for timestamp

Review Comment:
   nit `as-of` 
   
   why `for timestamp` 
   
   Or simpler: `The point in time for the query.` (Does not sound great either 
to be frank...)



##########
streams/src/main/java/org/apache/kafka/streams/state/internals/VersionedStoreQueryUtils.java:
##########
@@ -0,0 +1,189 @@
+/*
+ * 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 org.apache.kafka.streams.state.internals;
+
+import static org.apache.kafka.common.utils.Utils.mkEntry;
+import static org.apache.kafka.common.utils.Utils.mkMap;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.time.temporal.ChronoField;
+import java.util.Map;
+import org.apache.kafka.common.serialization.Deserializer;
+import org.apache.kafka.common.utils.Bytes;
+import org.apache.kafka.streams.processor.StateStore;
+import org.apache.kafka.streams.processor.StateStoreContext;
+import org.apache.kafka.streams.query.FailureReason;
+import org.apache.kafka.streams.query.Position;
+import org.apache.kafka.streams.query.PositionBound;
+import org.apache.kafka.streams.query.Query;
+import org.apache.kafka.streams.query.QueryConfig;
+import org.apache.kafka.streams.query.QueryResult;
+import org.apache.kafka.streams.query.VersionedKeyQuery;
+import org.apache.kafka.streams.state.StateSerdes;
+import org.apache.kafka.streams.state.VersionedKeyValueStore;
+import org.apache.kafka.streams.state.VersionedRecord;
+
+public final class VersionedStoreQueryUtils {
+
+    /**
+     * a utility interface to facilitate stores' query dispatch logic,
+     * allowing them to generically store query execution logic as the values
+     * in a map.
+     */
+    @FunctionalInterface
+    public interface QueryHandler {
+        QueryResult<?> apply(
+            final Query<?> query,
+            final PositionBound positionBound,
+            final QueryConfig config,
+            final StateStore store
+        );
+    }
+
+    @SuppressWarnings("rawtypes")
+    private static final Map<Class, QueryHandler> QUERY_HANDLER_MAP =
+        mkMap(
+            mkEntry(
+                VersionedKeyQuery.class,
+                VersionedStoreQueryUtils::runVersionedKeyQuery
+            )
+        );
+
+    // make this class uninstantiable
+
+    private VersionedStoreQueryUtils() {
+    }
+    @SuppressWarnings("unchecked")
+    public static <R> QueryResult<R> handleBasicQueries(
+        final Query<R> query,
+        final PositionBound positionBound,
+        final QueryConfig config,
+        final StateStore store,
+        final Position position,
+        final StateStoreContext context
+    ) {
+
+        final long start = config.isCollectExecutionInfo() ? System.nanoTime() 
: -1L;
+        final QueryResult<R> result;
+
+        final QueryHandler handler = QUERY_HANDLER_MAP.get(query.getClass());
+        if (handler == null) {
+            result = QueryResult.forUnknownQueryType(query, store);
+        } else if (context == null || !isPermitted(position, positionBound, 
context.taskId().partition())) {
+            result = QueryResult.notUpToBound(
+                position,
+                positionBound,
+                context == null ? null : context.taskId().partition()
+            );
+        } else {
+            result = (QueryResult<R>) handler.apply(
+                query,
+                positionBound,
+                config,
+                store
+            );
+        }
+        if (config.isCollectExecutionInfo()) {
+            result.addExecutionInfo(
+                "Handled in " + store.getClass() + " in " + (System.nanoTime() 
- start) + "ns"
+            );
+        }
+        result.setPosition(position);
+        return result;
+    }
+
+    public static boolean isPermitted(
+        final Position position,
+        final PositionBound positionBound,
+        final int partition
+    ) {
+        final Position bound = positionBound.position();
+        for (final String topic : bound.getTopics()) {
+            final Map<Integer, Long> partitionBounds = 
bound.getPartitionPositions(topic);
+            final Map<Integer, Long> seenPartitionPositions = 
position.getPartitionPositions(topic);
+            if (!partitionBounds.containsKey(partition)) {
+                // this topic isn't bounded for our partition, so just skip 
over it.
+            } else {
+                if (!seenPartitionPositions.containsKey(partition)) {
+                    // we haven't seen a partition that we have a bound for
+                    return false;
+                } else if (seenPartitionPositions.get(partition) < 
partitionBounds.get(partition)) {
+                    // our current position is behind the bound
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    @SuppressWarnings("unchecked")
+    private static <R> QueryResult<R> runVersionedKeyQuery(final Query<R> 
query,
+                                                  final PositionBound 
positionBound,
+                                                  final QueryConfig config,
+                                                  final StateStore store) {

Review Comment:
   nit: fix indention



##########
streams/src/main/java/org/apache/kafka/streams/state/internals/MeteredVersionedKeyValueStore.java:
##########
@@ -148,13 +199,33 @@ protected <R> QueryResult<R> runRangeQuery(final Query<R> 
query,
             throw new UnsupportedOperationException("Versioned stores do not 
support RangeQuery queries at this time.");
         }
 
-        @Override
-        protected <R> QueryResult<R> runKeyQuery(final Query<R> query,

Review Comment:
   Related to above: we should keep this method



##########
streams/src/main/java/org/apache/kafka/streams/state/internals/MeteredVersionedKeyValueStore.java:
##########
@@ -148,13 +205,38 @@ protected <R> QueryResult<R> runRangeQuery(final Query<R> 
query,
             throw new UnsupportedOperationException("Versioned stores do not 
support RangeQuery queries at this time.");
         }
 
+        @SuppressWarnings("unchecked")
         @Override
         protected <R> QueryResult<R> runKeyQuery(final Query<R> query,
-                                                 final PositionBound 
positionBound,
-                                                 final QueryConfig config) {
-            // throw exception for now to reserve the ability to implement 
this in the future
-            // without clashing with users' custom implementations in the 
meantime
-            throw new UnsupportedOperationException("Versioned stores do not 
support KeyQuery queries at this time.");
+            final PositionBound positionBound,
+            final QueryConfig config) {
+            if (query instanceof VersionedKeyQuery) {
+                final QueryResult<R> result;
+                final VersionedKeyQuery<K, V> typedKeyQuery = 
(VersionedKeyQuery<K, V>) query;
+                VersionedKeyQuery<Bytes, byte[]> rawKeyQuery;
+                if (typedKeyQuery.asOfTimestamp().isPresent()) {
+                    rawKeyQuery = 
VersionedKeyQuery.withKey(keyBytes(typedKeyQuery.key()));
+                    rawKeyQuery = 
rawKeyQuery.asOf(typedKeyQuery.asOfTimestamp().get());
+                } else {
+                    rawKeyQuery = 
VersionedKeyQuery.withKey(keyBytes(typedKeyQuery.key()));
+                }
+                final QueryResult<VersionedRecord<byte[]>> rawResult =
+                    wrapped().query(rawKeyQuery, positionBound, config);
+                if (rawResult.isSuccess()) {
+                    final Function<byte[], ValueAndTimestamp<V>> deserializer 
= getDeserializeValue(plainValueSerdes);

Review Comment:
   > MeteredVersionedKeyValueStoreInternal every other method uses 
ValueAndTimestamp as well.
   
   This part is for DSL compatibility only. All DSL operator only work with 
`ValueAndTimestamp` type right now, so when we plugin the new versioned state 
store, we need to translate between the types to make it work.
   
   > Btw, do you agree to implement a new class VersionedRecordDeserializer?
   
   As this point, I don't think we need such a class? The helper method you 
added does the job perfectly fine IMHO. But I am also not against it, if it 
make it easier (we would add it as internal class though only I assume?).



##########
streams/src/main/java/org/apache/kafka/streams/state/internals/MeteredVersionedKeyValueStore.java:
##########
@@ -148,13 +205,38 @@ protected <R> QueryResult<R> runRangeQuery(final Query<R> 
query,
             throw new UnsupportedOperationException("Versioned stores do not 
support RangeQuery queries at this time.");
         }
 
+        @SuppressWarnings("unchecked")
         @Override
         protected <R> QueryResult<R> runKeyQuery(final Query<R> query,

Review Comment:
   > I think we do not have simple key queries with versioned state stores
   
   Not right now, but I think we could add support for `KeyQuery` and 
`RangeQuery` and target the "latest segment" only, ie `KeyQuery == 
VersionedKeyQuery(w/o asOfTimestamp)` in the future and should keep the door 
open for this.



-- 
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: jira-unsubscr...@kafka.apache.org

For queries about this service, please contact Infrastructure at:
us...@infra.apache.org

Reply via email to