This is an automated email from the ASF dual-hosted git repository.

anovikov pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git


The following commit(s) were added to refs/heads/main by this push:
     new 1263f6b92e IGNITE-20879 Additional criterions for queries (#2933)
1263f6b92e is described below

commit 1263f6b92ed18b05d885961edd615e00a39929cc
Author: Andrey Novikov <anovi...@apache.org>
AuthorDate: Wed Jan 10 22:47:26 2024 +0700

    IGNITE-20879 Additional criterions for queries (#2933)
---
 .../table/criteria/{Criteria.java => Column.java}  |  34 +-
 .../criteria/{Criteria.java => Condition.java}     |  39 ++-
 .../org/apache/ignite/table/criteria/Criteria.java | 367 ++++++++++++++++++++-
 .../ignite/table/criteria/CriteriaVisitor.java     |  59 ++++
 .../apache/ignite/table/criteria/Expression.java   |  66 ++++
 .../criteria/{Criteria.java => Operator.java}      |  26 +-
 .../criteria/{Criteria.java => Parameter.java}     |  34 +-
 .../internal/client/table/AbstractClientView.java  |  78 +++++
 .../client/table/ClientRecordBinaryView.java       |  32 +-
 .../internal/client/table/ClientRecordView.java    |  32 +-
 .../internal/table/criteria/ColumnValidator.java   |  64 ++++
 .../internal/table/criteria/SqlSerializer.java     | 240 ++++++++++++++
 .../internal/ClusterPerClassIntegrationTest.java   |  27 +-
 .../ignite/internal/table/ItCriteriaQueryTest.java | 230 ++++++++++---
 .../ignite/internal/table/AbstractTableView.java   |  19 ++
 .../internal/table/RecordBinaryViewImpl.java       |  17 +-
 .../ignite/internal/table/RecordViewImpl.java      |  16 +-
 .../internal/table/criteria/SqlSerializerTest.java | 208 ++++++++++++
 18 files changed, 1460 insertions(+), 128 deletions(-)

diff --git 
a/modules/api/src/main/java/org/apache/ignite/table/criteria/Criteria.java 
b/modules/api/src/main/java/org/apache/ignite/table/criteria/Column.java
similarity index 55%
copy from 
modules/api/src/main/java/org/apache/ignite/table/criteria/Criteria.java
copy to modules/api/src/main/java/org/apache/ignite/table/criteria/Column.java
index 1734c30157..21c60daee3 100644
--- a/modules/api/src/main/java/org/apache/ignite/table/criteria/Criteria.java
+++ b/modules/api/src/main/java/org/apache/ignite/table/criteria/Column.java
@@ -17,9 +17,37 @@
 
 package org.apache.ignite.table.criteria;
 
+import static org.apache.ignite.lang.util.IgniteNameUtils.parseSimpleName;
+
+import org.jetbrains.annotations.Nullable;
+
 /**
- * Represents a criteria query predicate.
+ * Represents a column reference for criteria query.
  */
-public interface Criteria {
-    // No-op.
+public final class Column implements Criteria {
+    private final String name;
+
+    /**
+     * Constructor.
+     *
+     * @param name A column name.
+     */
+    Column(String name) {
+        this.name = parseSimpleName(name);
+    }
+
+    /**
+     * Gets column name.
+     *
+     * @return A column name.
+     */
+    public String getName() {
+        return name;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public <C> void accept(CriteriaVisitor<C> v, @Nullable C context) {
+        v.visit(this, context);
+    }
 }
diff --git 
a/modules/api/src/main/java/org/apache/ignite/table/criteria/Criteria.java 
b/modules/api/src/main/java/org/apache/ignite/table/criteria/Condition.java
similarity index 52%
copy from 
modules/api/src/main/java/org/apache/ignite/table/criteria/Criteria.java
copy to 
modules/api/src/main/java/org/apache/ignite/table/criteria/Condition.java
index 1734c30157..2bae02c9a8 100644
--- a/modules/api/src/main/java/org/apache/ignite/table/criteria/Criteria.java
+++ b/modules/api/src/main/java/org/apache/ignite/table/criteria/Condition.java
@@ -18,8 +18,41 @@
 package org.apache.ignite.table.criteria;
 
 /**
- * Represents a criteria query predicate.
+ * Represents an condition with operator and elements for criteria query.
+ *
+ * @see Criteria
  */
-public interface Criteria {
-    // No-op.
+public final class Condition {
+    private final Operator operator;
+
+    private final Criteria[] elements;
+
+    /**
+     * Constructor.
+     *
+     * @param operator Condition operator.
+     * @param elements Condition elements.
+     */
+    Condition(Operator operator, Criteria... elements) {
+        this.operator = operator;
+        this.elements = elements;
+    }
+
+    /**
+     * Get a condition operator.
+     *
+     * @return A condition operator.
+     */
+    public Operator getOperator() {
+        return operator;
+    }
+
+    /**
+     * Get a condition elements.
+     *
+     * @return A condition elements.
+     */
+    public Criteria[] getElements() {
+        return elements;
+    }
 }
diff --git 
a/modules/api/src/main/java/org/apache/ignite/table/criteria/Criteria.java 
b/modules/api/src/main/java/org/apache/ignite/table/criteria/Criteria.java
index 1734c30157..1533fa908c 100644
--- a/modules/api/src/main/java/org/apache/ignite/table/criteria/Criteria.java
+++ b/modules/api/src/main/java/org/apache/ignite/table/criteria/Criteria.java
@@ -17,9 +17,374 @@
 
 package org.apache.ignite.table.criteria;
 
+import static java.util.Objects.requireNonNull;
+
+import java.util.Arrays;
+import java.util.Objects;
+import org.jetbrains.annotations.Nullable;
+
 /**
  * Represents a criteria query predicate.
+ *
+ * <pre>{@code
+ *      public ClosableCursor<Product> uncategorizedProducts() {
+ *         return products.recordView(Product.class).queryCriteria(null, 
columnValue("category", nullValue()));
+ *      }
+ * }</pre>
+ *
+ * @see CriteriaQuerySource
  */
 public interface Criteria {
-    // No-op.
+    /**
+     * Accept the visitor with the given context.
+     *
+     * @param <C> Context type.
+     * @param v Visitor.
+     * @param context Context of visit.
+     */
+    <C> void accept(CriteriaVisitor<C> v, @Nullable C context);
+
+    /**
+     * Creates a predicate that tests whether the column value is equal to the 
given condition.
+     *
+     * <p>For example:
+     * <pre>{@code
+     *     columnValue("category", equalTo("toys"))
+     *     columnValue(IgniteNameUtils.quote("subCategory"), equalTo("puzzle"))
+     * }</pre>
+     *
+     * @param columnName Column name must use SQL-parser style notation; e.g., 
<br>
+     *                   "myColumn", creates a predicate for the column 
ignores case sensitivity, <br>
+     *                   "\"MyColumn\"", creates a predicate for the column 
with respect to case sensitivity.
+     * @param condition Target condition.
+     * @return The created expression instance.
+     */
+    static Expression columnValue(String columnName, Condition condition) {
+        Criteria[] oldElements = condition.getElements();
+        var newElements = new Criteria[oldElements.length + 1];
+
+        newElements[0] = new Column(columnName);
+        System.arraycopy(oldElements, 0, newElements, 1, oldElements.length);
+
+        return new Expression(condition.getOperator(), newElements);
+    }
+
+    /**
+     * Creates the negation of the predicate.
+     *
+     * <p>For example:
+     * <pre>{@code
+     *     not(columnValue("category", equalTo("toys")))
+     * }</pre>
+     *
+     * @param expression Expression.
+     * @return The created negation of the expression.
+     */
+    static Expression not(Expression expression) {
+        requireNonNull(expression, "expression must not be null");
+
+        return new Expression(Operator.NOT, expression);
+    }
+
+    /**
+     * Creates the {@code and} of the expressions.
+     *
+     * <p>For example:
+     * <pre>{@code
+     *     and(columnValue("category", equalTo("toys")), 
columnValue("quantity", lessThan(20)))
+     * }</pre>
+     *
+     * @param expressions Expressions.
+     * @return The created {@code and} expression instance.
+     */
+    static Expression and(Expression... expressions) {
+        if (expressions == null || expressions.length == 0 || 
Arrays.stream(expressions).anyMatch(Objects::isNull)) {
+            throw new IllegalArgumentException("expressions must not be empty 
or null");
+        }
+
+        return new Expression(Operator.AND, expressions);
+    }
+
+    /**
+     * Creates the {@code or} of the expressions.
+     *
+     * <p>For example:
+     * <pre>{@code
+     *     or(columnValue("category", equalTo("toys")), 
columnValue("category", equalTo("games")))
+     * }</pre>
+     *
+     * @param expressions Expressions.
+     * @return The created {@code or} expressions instance.
+     */
+    static Expression or(Expression... expressions) {
+        if (expressions == null || expressions.length == 0 || 
Arrays.stream(expressions).anyMatch(Objects::isNull)) {
+            throw new IllegalArgumentException("expressions must not be empty 
or null");
+        }
+
+        return new Expression(Operator.OR, expressions);
+    }
+
+    /**
+     * Creates a condition that test the examined object is equal to the 
specified {@code value}.
+     *
+     * <p>For example:
+     * <pre>{@code
+     *     columnValue("category", equalTo("toys"))
+     * }</pre>
+     *
+     * @param <T> Value type.
+     * @param value Target value.
+     */
+    static <T> Condition equalTo(Comparable<T> value) {
+        if (value == null) {
+            return nullValue();
+        }
+
+        return new Condition(Operator.EQ, new Parameter<>(value));
+    }
+
+    /**
+     * Creates a condition that test the examined object is equal to the 
specified {@code value}.
+     *
+     * <p>For example:
+     * <pre>{@code
+     *     columnValue("password", equalTo("MyPassword".getBytes()))
+     * }</pre>
+     *
+     * @param value Target value.
+     */
+    static Condition equalTo(byte[] value) {
+        if (value == null) {
+            return nullValue();
+        }
+
+        return new Condition(Operator.EQ, new Parameter<>(value));
+    }
+
+    /**
+     * Creates a condition that test the examined object is not equal to the 
specified {@code value}.
+     *
+     * <p>For example:
+     * <pre>{@code
+     *     columnValue("category", notEqualTo("toys"))
+     * }</pre>
+     *
+     * @param <T> Value type.
+     * @param value Target value.
+     */
+    static <T> Condition notEqualTo(Comparable<T> value) {
+        if (value == null) {
+            return notNullValue();
+        }
+
+        return new Condition(Operator.NOT_EQ, new Parameter<>(value));
+    }
+
+    /**
+     * Creates a condition that test the examined object is not equal to the 
specified {@code value}.
+     *
+     * <p>For example:
+     * <pre>{@code
+     *     columnValue("password", notEqualTo("MyPassword".getBytes()))
+     * }</pre>
+     *
+     * @param value Target value.
+     */
+    static Condition notEqualTo(byte[] value) {
+        if (value == null) {
+            return notNullValue();
+        }
+
+        return new Condition(Operator.NOT_EQ, new Parameter<>(value));
+    }
+
+    /**
+     * Creates a condition that test the examined object is greater than the 
specified {@code value}.
+     *
+     * <p>For example:
+     * <pre>{@code
+     *     columnValue("age", greaterThan(35))
+     * }</pre>
+     *
+     * @param <T> Value type.
+     * @param value Target value.
+     */
+    static <T> Condition greaterThan(Comparable<T> value) {
+        return new Condition(Operator.GT, new Parameter<>(value));
+    }
+
+    /**
+     * Creates a condition that test the examined object is greater than or 
equal than the specified {@code value}.
+     *
+     * <p>For example:
+     * <pre>{@code
+     *     columnValue("age", greaterThanOrEqualTo(35))
+     * }</pre>
+     *
+     * @param <T> Value type.
+     * @param value Target value.
+     */
+    static <T> Condition greaterThanOrEqualTo(Comparable<T> value) {
+        return new Condition(Operator.GOE, new Parameter<>(value));
+    }
+
+    /**
+     * Creates a condition that test the examined object is less than the 
specified {@code value}.
+     *
+     * <p>For example:
+     * <pre>{@code
+     *     columnValue("age", lessThan(35))
+     * }</pre>
+     *
+     * @param <T> Value type.
+     * @param value Target value.
+     */
+    static <T> Condition lessThan(Comparable<T> value) {
+        return new Condition(Operator.LT, new Parameter<>(value));
+    }
+
+    /**
+     * Creates a condition that test the examined object is less than or equal 
than the specified {@code value}.
+     *
+     * <p>For example:
+     * <pre>{@code
+     *     columnValue("age", lessThanOrEqualTo(35))
+     * }</pre>
+     *
+     * @param <T> Value type.
+     * @param value Target value.
+     */
+    static <T> Condition lessThanOrEqualTo(Comparable<T> value) {
+        return new Condition(Operator.LOE, new Parameter<>(value));
+    }
+
+    /**
+     * Creates a condition that test the examined object is null.
+     *
+     * <p>For example:
+     * <pre>{@code
+     *     columnValue("category", nullValue())
+     * }</pre>
+     */
+    static Condition nullValue() {
+        return new Condition(Operator.IS_NULL);
+    }
+
+    /**
+     * Creates a condition that test the examined object is not null.
+     *
+     * <p>For example:
+     * <pre>{@code
+     *     columnValue("category", notNullValue())
+     * }</pre>
+     */
+    static Condition notNullValue() {
+        return new Condition(Operator.IS_NOT_NULL);
+    }
+
+    /**
+     * Creates a condition that test the examined object is is found within 
the specified {@code collection}.
+     *
+     * <p>For example:
+     * <pre>{@code
+     *     columnValue("category", in("toys", "games"))
+     * }</pre>
+     *
+     * @param <T> Values type.
+     * @param values The collection in which matching items must be found.
+     */
+    static <T> Condition in(Comparable<T>... values) {
+        if (values.length == 0) {
+            throw new IllegalArgumentException("values must not be empty or 
null");
+        }
+
+        if (values.length == 1) {
+            return equalTo(values[0]);
+        }
+
+        Criteria[] args = Arrays.stream(values)
+                .map(Parameter::new)
+                .toArray(Criteria[]::new);
+
+        return new Condition(Operator.IN, args);
+    }
+
+    /**
+     * Creates a condition that test the examined object is is found within 
the specified {@code collection}.
+     *
+     * <p>For example:
+     * <pre>{@code
+     *     columnValue("password", in("MyPassword".getBytes(), 
"MyOtherPassword".getBytes()))
+     * }</pre>
+     *
+     * @param values The collection in which matching items must be found.
+     */
+    static Condition in(byte[]... values) {
+        if (values.length == 0) {
+            throw new IllegalArgumentException("values must not be empty or 
null");
+        }
+
+        if (values.length == 1) {
+            return equalTo(values[0]);
+        }
+
+        Criteria[] args = Arrays.stream(values)
+                .map(Parameter::new)
+                .toArray(Criteria[]::new);
+
+        return new Condition(Operator.IN, args);
+    }
+
+    /**
+     * Creates a condition that test the examined object is is not found 
within the specified {@code collection}.
+     *
+     * <p>For example:
+     * <pre>{@code
+     *     columnValue("category", notIn("toys", "games"))
+     * }</pre>
+     *
+     * @param <T> Values type.
+     * @param values The collection in which matching items must be not found.
+     */
+    static <T> Condition notIn(Comparable<T>... values) {
+        if (values.length == 0) {
+            throw new IllegalArgumentException("values must not be empty or 
null");
+        }
+
+        if (values.length == 1) {
+            return notEqualTo(values[0]);
+        }
+
+        Criteria[] args = Arrays.stream(values)
+                .map(Parameter::new)
+                .toArray(Criteria[]::new);
+
+        return new Condition(Operator.NOT_IN, args);
+    }
+
+    /**
+     * Creates a condition that test the examined object is is not found 
within the specified {@code collection}.
+     *
+     * <p>For example:
+     * <pre>{@code
+     *     columnValue("password", notIn("MyPassword".getBytes(), 
"MyOtherPassword".getBytes()))
+     * }</pre>
+     *
+     * @param values The collection in which matching items must be not found.
+     */
+    static Condition notIn(byte[]... values) {
+        if (values.length == 0) {
+            throw new IllegalArgumentException("values must not be empty or 
null");
+        }
+
+        if (values.length == 1) {
+            return notEqualTo(values[0]);
+        }
+
+        Criteria[] args = Arrays.stream(values)
+                .map(Parameter::new)
+                .toArray(Criteria[]::new);
+
+        return new Condition(Operator.NOT_IN, args);
+    }
 }
diff --git 
a/modules/api/src/main/java/org/apache/ignite/table/criteria/CriteriaVisitor.java
 
b/modules/api/src/main/java/org/apache/ignite/table/criteria/CriteriaVisitor.java
new file mode 100644
index 0000000000..a40742b0cb
--- /dev/null
+++ 
b/modules/api/src/main/java/org/apache/ignite/table/criteria/CriteriaVisitor.java
@@ -0,0 +1,59 @@
+/*
+ * 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.ignite.table.criteria;
+
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * A visitor to traverse an criteria tree.
+ *
+ * @param <C> Context type.
+ */
+public interface CriteriaVisitor<C> {
+    /**
+     * Visit a {@link Parameter} instance with the given context.
+     *
+     * @param argument Parameter to visit
+     * @param context context of the visit or {@code null}, if not used
+     */
+    <T> void visit(Parameter<T> argument, @Nullable C context);
+
+    /**
+     * Visit a {@link Column} instance with the given context.
+     *
+     * @param column Column to visit
+     * @param context context of the visit or {@code null}, if not used
+     */
+    <T> void visit(Column column, @Nullable C context);
+
+    /**
+     * Visit a {@link Expression} instance with the given context.
+     *
+     * @param expression Expression to visit
+     * @param context context of the visit or {@code null}, if not used
+     */
+    <T> void visit(Expression expression, @Nullable C context);
+
+    /**
+     * Visit a {@link Criteria} instance with the given context.
+     *
+     * @param criteria Criteria to visit
+     * @param context context of the visit or {@code null}, if not used
+     */
+    <T> void visit(Criteria criteria, @Nullable C context);
+}
diff --git 
a/modules/api/src/main/java/org/apache/ignite/table/criteria/Expression.java 
b/modules/api/src/main/java/org/apache/ignite/table/criteria/Expression.java
new file mode 100644
index 0000000000..913f36f7f7
--- /dev/null
+++ b/modules/api/src/main/java/org/apache/ignite/table/criteria/Expression.java
@@ -0,0 +1,66 @@
+/*
+ * 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.ignite.table.criteria;
+
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Defines a expression for a criteria query with operator and it's arguments.
+ *
+ * @see Criteria
+ */
+public final class Expression implements Criteria {
+    private final Operator operator;
+
+    private final Criteria[] elements;
+
+    /**
+     * Constructor.
+     *
+     * @param operator Operator.
+     * @param elements Criteria elements.
+     */
+    Expression(Operator operator, Criteria... elements) {
+        this.operator = operator;
+        this.elements = elements;
+    }
+
+    /**
+     * Get a operator.
+     *
+     * @return A operator.
+     */
+    public Operator getOperator() {
+        return operator;
+    }
+
+    /**
+     * Get a condition elements.
+     *
+     * @return A condition elements.
+     */
+    public Criteria[] getElements() {
+        return elements;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public <C> void accept(CriteriaVisitor<C> v, @Nullable C context) {
+        v.visit(this, context);
+    }
+}
diff --git 
a/modules/api/src/main/java/org/apache/ignite/table/criteria/Criteria.java 
b/modules/api/src/main/java/org/apache/ignite/table/criteria/Operator.java
similarity index 74%
copy from 
modules/api/src/main/java/org/apache/ignite/table/criteria/Criteria.java
copy to modules/api/src/main/java/org/apache/ignite/table/criteria/Operator.java
index 1734c30157..bf760fd4f0 100644
--- a/modules/api/src/main/java/org/apache/ignite/table/criteria/Criteria.java
+++ b/modules/api/src/main/java/org/apache/ignite/table/criteria/Operator.java
@@ -18,8 +18,28 @@
 package org.apache.ignite.table.criteria;
 
 /**
- * Represents a criteria query predicate.
+ * Provides the operators for the criteria query grammar.
+ *
+ * @see Condition
+ * @see Expression
  */
-public interface Criteria {
-    // No-op.
+public enum Operator {
+    // General
+    EQ,
+    NOT_EQ,
+    IS_NULL,
+    IS_NOT_NULL,
+
+    // Comparable
+    GOE,
+    GT,
+    LOE,
+    LT,
+    IN,
+    NOT_IN,
+
+    // Boolean
+    NOT,
+    AND,
+    OR
 }
diff --git 
a/modules/api/src/main/java/org/apache/ignite/table/criteria/Criteria.java 
b/modules/api/src/main/java/org/apache/ignite/table/criteria/Parameter.java
similarity index 57%
copy from 
modules/api/src/main/java/org/apache/ignite/table/criteria/Criteria.java
copy to 
modules/api/src/main/java/org/apache/ignite/table/criteria/Parameter.java
index 1734c30157..07ce22b12a 100644
--- a/modules/api/src/main/java/org/apache/ignite/table/criteria/Criteria.java
+++ b/modules/api/src/main/java/org/apache/ignite/table/criteria/Parameter.java
@@ -17,9 +17,37 @@
 
 package org.apache.ignite.table.criteria;
 
+import org.jetbrains.annotations.Nullable;
+
 /**
- * Represents a criteria query predicate.
+ * Represents a parameter for criteria query.
+ *
+ * @param <T> Parameter type.
  */
-public interface Criteria {
-    // No-op.
+public final class Parameter<T> implements Criteria {
+    private final T value;
+
+    /**
+     * Constructor.
+     *
+     * @param value Parameter value.
+     */
+    Parameter(T value) {
+        this.value = value;
+    }
+
+    /**
+     * Gets parameter value.
+     *
+     * @return A value.
+     */
+    public T getValue() {
+        return value;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public <C> void accept(CriteriaVisitor<C> v, @Nullable C context) {
+        v.visit(this, context);
+    }
 }
diff --git 
a/modules/client/src/main/java/org/apache/ignite/internal/client/table/AbstractClientView.java
 
b/modules/client/src/main/java/org/apache/ignite/internal/client/table/AbstractClientView.java
new file mode 100644
index 0000000000..c7eabdb65e
--- /dev/null
+++ 
b/modules/client/src/main/java/org/apache/ignite/internal/client/table/AbstractClientView.java
@@ -0,0 +1,78 @@
+/*
+ * 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.ignite.internal.client.table;
+
+import static java.util.stream.Collectors.toSet;
+import static org.apache.ignite.internal.client.ClientUtils.sync;
+import static org.apache.ignite.lang.util.IgniteNameUtils.parseSimpleName;
+
+import java.util.Arrays;
+import java.util.Set;
+import org.apache.ignite.internal.table.criteria.CursorAdapter;
+import org.apache.ignite.internal.table.criteria.SqlSerializer;
+import org.apache.ignite.lang.Cursor;
+import org.apache.ignite.table.criteria.Criteria;
+import org.apache.ignite.table.criteria.CriteriaQueryOptions;
+import org.apache.ignite.table.criteria.CriteriaQuerySource;
+import org.apache.ignite.tx.Transaction;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Base class for client views.
+ */
+abstract class AbstractClientView<T> implements CriteriaQuerySource<T> {
+    /** Underlying table. */
+    protected final ClientTable tbl;
+
+    /**
+     * Constructor.
+     *
+     * @param tbl Underlying table.
+     */
+    AbstractClientView(ClientTable tbl) {
+        assert tbl != null;
+
+        this.tbl = tbl;
+    }
+
+    /**
+     * Construct SQL query and arguments for prepare statement.
+     *
+     * @param tableName Table name.
+     * @param columns Table columns.
+     * @param criteria The predicate to filter entries or {@code null} to 
return all entries from the underlying table.
+     * @return SQL query and it's arguments.
+     */
+    protected static SqlSerializer createSqlSerializer(String tableName, 
ClientColumn[] columns, @Nullable Criteria criteria) {
+        Set<String> columnNames = Arrays.stream(columns)
+                .map(ClientColumn::name)
+                .collect(toSet());
+
+        return new SqlSerializer.Builder()
+                .tableName(parseSimpleName(tableName))
+                .columns(columnNames)
+                .where(criteria)
+                .build();
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Cursor<T> query(@Nullable Transaction tx, @Nullable Criteria 
criteria, @Nullable CriteriaQueryOptions opts) {
+        return new CursorAdapter<>(sync(queryAsync(tx, criteria, opts)));
+    }
+}
diff --git 
a/modules/client/src/main/java/org/apache/ignite/internal/client/table/ClientRecordBinaryView.java
 
b/modules/client/src/main/java/org/apache/ignite/internal/client/table/ClientRecordBinaryView.java
index 1646367394..5fb64df33d 100644
--- 
a/modules/client/src/main/java/org/apache/ignite/internal/client/table/ClientRecordBinaryView.java
+++ 
b/modules/client/src/main/java/org/apache/ignite/internal/client/table/ClientRecordBinaryView.java
@@ -32,10 +32,9 @@ import org.apache.ignite.internal.client.proto.ClientOp;
 import org.apache.ignite.internal.client.sql.ClientSessionBuilder;
 import org.apache.ignite.internal.client.sql.ClientStatementBuilder;
 import org.apache.ignite.internal.streamer.StreamerBatchSender;
-import org.apache.ignite.internal.table.criteria.CursorAdapter;
 import org.apache.ignite.internal.table.criteria.QueryCriteriaAsyncCursor;
+import org.apache.ignite.internal.table.criteria.SqlSerializer;
 import org.apache.ignite.lang.AsyncCursor;
-import org.apache.ignite.lang.Cursor;
 import org.apache.ignite.sql.Session;
 import org.apache.ignite.sql.Statement;
 import org.apache.ignite.table.DataStreamerOptions;
@@ -49,10 +48,7 @@ import org.jetbrains.annotations.Nullable;
 /**
  * Client record view implementation for binary user-object representation.
  */
-public class ClientRecordBinaryView implements RecordView<Tuple> {
-    /** Underlying table. */
-    private final ClientTable tbl;
-
+public class ClientRecordBinaryView extends AbstractClientView<Tuple> 
implements RecordView<Tuple> {
     /** Tuple serializer. */
     private final ClientTupleSerializer ser;
 
@@ -62,9 +58,8 @@ public class ClientRecordBinaryView implements 
RecordView<Tuple> {
      * @param tbl Table.
      */
     public ClientRecordBinaryView(ClientTable tbl) {
-        assert tbl != null;
+        super(tbl);
 
-        this.tbl = tbl;
         ser = new ClientTupleSerializer(tbl.tableId());
     }
 
@@ -390,12 +385,6 @@ public class ClientRecordBinaryView implements 
RecordView<Tuple> {
         return ClientDataStreamer.streamData(publisher, opts, batchSender, 
provider, tbl);
     }
 
-    /** {@inheritDoc} */
-    @Override
-    public Cursor<Tuple> query(@Nullable Transaction tx, @Nullable Criteria 
criteria,  @Nullable CriteriaQueryOptions opts) {
-        return new CursorAdapter<>(sync(queryAsync(tx, criteria, opts)));
-    }
-
     /** {@inheritDoc} */
     @Override
     public CompletableFuture<AsyncCursor<Tuple>> queryAsync(
@@ -403,14 +392,17 @@ public class ClientRecordBinaryView implements 
RecordView<Tuple> {
             @Nullable Criteria criteria,
             @Nullable CriteriaQueryOptions opts
     ) {
-        //TODO: implement serialization of criteria to SQL 
https://issues.apache.org/jira/browse/IGNITE-20879
-        var query = "SELECT * FROM " + tbl.name();
         var opts0 = opts == null ? CriteriaQueryOptions.DEFAULT : opts;
 
-        Statement statement = new 
ClientStatementBuilder().query(query).pageSize(opts0.pageSize()).build();
-        Session session = new ClientSessionBuilder(tbl.channel()).build();
+        return tbl.getLatestSchema()
+                .thenCompose((schema) -> {
+                    SqlSerializer ser = createSqlSerializer(tbl.name(), 
schema.columns(), criteria);
+
+                    Statement statement = new 
ClientStatementBuilder().query(ser.toString()).pageSize(opts0.pageSize()).build();
+                    Session session = new 
ClientSessionBuilder(tbl.channel()).build();
 
-        return session.executeAsync(tx, statement)
-                .thenApply(resultSet -> new 
QueryCriteriaAsyncCursor<>(resultSet, session::close));
+                    return session.executeAsync(tx, statement, 
ser.getArguments())
+                            .thenApply(resultSet -> new 
QueryCriteriaAsyncCursor<>(resultSet, session::close));
+                });
     }
 }
diff --git 
a/modules/client/src/main/java/org/apache/ignite/internal/client/table/ClientRecordView.java
 
b/modules/client/src/main/java/org/apache/ignite/internal/client/table/ClientRecordView.java
index 5a2b7eee97..37a2507e71 100644
--- 
a/modules/client/src/main/java/org/apache/ignite/internal/client/table/ClientRecordView.java
+++ 
b/modules/client/src/main/java/org/apache/ignite/internal/client/table/ClientRecordView.java
@@ -33,10 +33,9 @@ import org.apache.ignite.internal.client.proto.TuplePart;
 import org.apache.ignite.internal.client.sql.ClientSessionBuilder;
 import org.apache.ignite.internal.client.sql.ClientStatementBuilder;
 import org.apache.ignite.internal.streamer.StreamerBatchSender;
-import org.apache.ignite.internal.table.criteria.CursorAdapter;
 import org.apache.ignite.internal.table.criteria.QueryCriteriaAsyncCursor;
+import org.apache.ignite.internal.table.criteria.SqlSerializer;
 import org.apache.ignite.lang.AsyncCursor;
-import org.apache.ignite.lang.Cursor;
 import org.apache.ignite.sql.Session;
 import org.apache.ignite.sql.Statement;
 import org.apache.ignite.table.DataStreamerOptions;
@@ -50,10 +49,7 @@ import org.jetbrains.annotations.Nullable;
 /**
  * Client record view implementation.
  */
-public class ClientRecordView<R> implements RecordView<R> {
-    /** Underlying table. */
-    private final ClientTable tbl;
-
+public class ClientRecordView<R> extends AbstractClientView<R> implements 
RecordView<R> {
     /** Serializer. */
     private final ClientRecordSerializer<R> ser;
 
@@ -64,7 +60,8 @@ public class ClientRecordView<R> implements RecordView<R> {
      * @param recMapper Mapper.
      */
     ClientRecordView(ClientTable tbl, Mapper<R> recMapper) {
-        this.tbl = tbl;
+        super(tbl);
+
         ser = new ClientRecordSerializer<>(tbl.tableId(), recMapper);
     }
 
@@ -386,12 +383,6 @@ public class ClientRecordView<R> implements RecordView<R> {
         return ClientDataStreamer.streamData(publisher, opts, batchSender, 
provider, tbl);
     }
 
-    /** {@inheritDoc} */
-    @Override
-    public Cursor<R> query(@Nullable Transaction tx, @Nullable Criteria 
criteria, @Nullable CriteriaQueryOptions opts) {
-        return new CursorAdapter<>(sync(queryAsync(tx, criteria, opts)));
-    }
-
     /** {@inheritDoc} */
     @Override
     public CompletableFuture<AsyncCursor<R>> queryAsync(
@@ -399,14 +390,17 @@ public class ClientRecordView<R> implements RecordView<R> 
{
             @Nullable Criteria criteria,
             @Nullable CriteriaQueryOptions opts
     ) {
-        //TODO: implement serialization of criteria to SQL 
https://issues.apache.org/jira/browse/IGNITE-20879
-        var query = "SELECT * FROM " + tbl.name();
         var opts0 = opts == null ? CriteriaQueryOptions.DEFAULT : opts;
 
-        Statement statement = new 
ClientStatementBuilder().query(query).pageSize(opts0.pageSize()).build();
-        Session session = new ClientSessionBuilder(tbl.channel()).build();
+        return tbl.getLatestSchema()
+                .thenCompose((schema) -> {
+                    SqlSerializer ser = createSqlSerializer(tbl.name(), 
schema.columns(), criteria);
+
+                    Statement statement = new 
ClientStatementBuilder().query(ser.toString()).pageSize(opts0.pageSize()).build();
+                    Session session = new 
ClientSessionBuilder(tbl.channel()).build();
 
-        return session.executeAsync(tx, ser.mapper(), statement)
-                .thenApply(resultSet -> new 
QueryCriteriaAsyncCursor<>(resultSet, session::close));
+                    return session.executeAsync(tx, this.ser.mapper(), 
statement, ser.getArguments())
+                            .thenApply(resultSet -> new 
QueryCriteriaAsyncCursor<>(resultSet, session::close));
+                });
     }
 }
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/table/criteria/ColumnValidator.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/table/criteria/ColumnValidator.java
new file mode 100644
index 0000000000..3ae839861e
--- /dev/null
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/table/criteria/ColumnValidator.java
@@ -0,0 +1,64 @@
+/*
+ * 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.ignite.internal.table.criteria;
+
+import static org.apache.ignite.internal.util.CollectionUtils.nullOrEmpty;
+
+import java.util.Collection;
+import org.apache.ignite.table.criteria.Column;
+import org.apache.ignite.table.criteria.Criteria;
+import org.apache.ignite.table.criteria.CriteriaVisitor;
+import org.apache.ignite.table.criteria.Expression;
+import org.apache.ignite.table.criteria.Parameter;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Column validator.
+ */
+class ColumnValidator implements CriteriaVisitor<Collection<String>> {
+    static final ColumnValidator INSTANCE = new ColumnValidator();
+
+    /** {@inheritDoc} */
+    @Override
+    public <T> void visit(Parameter<T> argument, @Nullable Collection<String> 
context) {
+        // No-op.
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public <T> void visit(Column column, @Nullable Collection<String> context) 
{
+        String colName = column.getName();
+
+        if (!nullOrEmpty(context) && !context.contains(colName)) {
+            throw new IllegalArgumentException("Unexpected column name: " + 
colName);
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public <T> void visit(Expression expression, @Nullable Collection<String> 
context) {
+        for (Criteria element : expression.getElements()) {
+            element.accept(this, context);
+        }
+    }
+
+    @Override
+    public <T> void visit(Criteria criteria, @Nullable Collection<String> 
context) {
+        criteria.accept(this, context);
+    }
+}
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/table/criteria/SqlSerializer.java
 
b/modules/core/src/main/java/org/apache/ignite/internal/table/criteria/SqlSerializer.java
new file mode 100644
index 0000000000..1b45243724
--- /dev/null
+++ 
b/modules/core/src/main/java/org/apache/ignite/internal/table/criteria/SqlSerializer.java
@@ -0,0 +1,240 @@
+/*
+ * 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.ignite.internal.table.criteria;
+
+import static org.apache.ignite.internal.util.StringUtils.nullOrBlank;
+import static org.apache.ignite.lang.util.IgniteNameUtils.quote;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.apache.ignite.internal.util.CollectionUtils;
+import org.apache.ignite.table.criteria.Column;
+import org.apache.ignite.table.criteria.Criteria;
+import org.apache.ignite.table.criteria.CriteriaVisitor;
+import org.apache.ignite.table.criteria.Expression;
+import org.apache.ignite.table.criteria.Operator;
+import org.apache.ignite.table.criteria.Parameter;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Serializes {@link Criteria} into into SQL.
+ *
+ * <p>Note: Doesn't required any context to traverse an criteria tree, {@code 
null} can be used as initial context.
+ */
+public class SqlSerializer implements CriteriaVisitor<Void> {
+    private static final Map<Operator, String> ELEMENT_TEMPLATES = Map.of(
+            Operator.EQ, "{0} = {1}",
+            Operator.NOT_EQ, "{0} <> {1}",
+            Operator.IS_NULL, "{0} IS NULL",
+            Operator.IS_NOT_NULL, "{0} IS NOT NULL",
+            Operator.GOE, "{0} >= {1}",
+            Operator.GT, "{0} > {1}",
+            Operator.LOE, "{0} <= {1}",
+            Operator.LT, "{0} < {1}",
+            Operator.NOT, "NOT ({0})"
+    );
+
+    private final Pattern pattern = Pattern.compile("\\{(\\d+)\\}");
+
+    @SuppressWarnings("StringBufferField")
+    private final StringBuilder builder = new StringBuilder(128);
+
+    private final List<Object> arguments = new LinkedList<>();
+
+    /**
+     * Get query arguments.
+     *
+     * @return Query arguments.
+     */
+    public Object[] getArguments() {
+        return arguments.toArray(new Object[0]);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public <T> void visit(Parameter<T> argument, @Nullable Void context) {
+        append("?");
+
+        arguments.add(argument.getValue());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public <T> void visit(Column column, @Nullable Void context) {
+        append(quoteIfNeeded(column.getName()));
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public <T> void visit(Expression expression, @Nullable Void context) {
+        Operator operator = expression.getOperator();
+        Criteria[] elements = expression.getElements();
+
+        if (operator == Operator.AND || operator == Operator.OR) {
+            append(operator == Operator.AND ? ") AND (" : ") OR (", "(", ")", 
elements, context);
+        } else if (operator == Operator.IN || operator == Operator.NOT_IN) {
+            elements[0].accept(this, context);
+            append(operator == Operator.IN ? " IN " : " NOT IN ");
+
+            Criteria[] tail = Arrays.copyOfRange(elements, 1, elements.length);
+            append(", ", "(", ")", tail, context);
+        } else {
+            String template = ELEMENT_TEMPLATES.get(operator);
+
+            int end = 0;
+            Matcher matcher = pattern.matcher(template);
+
+            while (matcher.find()) {
+                if (matcher.start() > end) {
+                    append(template.substring(end, matcher.start()));
+                }
+
+                int index = Integer.parseInt(matcher.group(1));
+                elements[index].accept(this, context);
+
+                end = matcher.end();
+            }
+
+            if (end < template.length()) {
+                append(template.substring(end));
+            }
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public <T> void visit(Criteria criteria, @Nullable Void context) {
+        criteria.accept(this, context);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String toString() {
+        return builder.toString();
+    }
+
+    private SqlSerializer append(String str) {
+        builder.append(str);
+
+        return this;
+    }
+
+    private void append(String delimiter, String prefix, String suffix, 
Criteria[] elements, @Nullable Void context) {
+        if (elements.length > 1) {
+            append(prefix);
+        }
+
+        for (int i = 0; i < elements.length; i++) {
+            elements[i].accept(this, context);
+
+            if (i < elements.length - 1) {
+                append(delimiter);
+            }
+        }
+
+        if (elements.length > 1) {
+            append(suffix);
+        }
+    }
+
+    private static String quoteIfNeeded(String name) {
+        return name.chars().allMatch(Character::isUpperCase) ? name : 
quote(name);
+    }
+
+    /**
+     * Builder.
+     */
+    public static class Builder  {
+        @Nullable
+        private String tableName;
+
+        @Nullable
+        private Collection<String> columnNames;
+
+        @Nullable
+        private Criteria where;
+
+        /**
+         * Sets the table name. Must be unquoted name or name is cast to upper 
case.
+         *
+         * @param tableName Table name.
+         * @return This builder instance.
+         */
+        public SqlSerializer.Builder tableName(String tableName) {
+            this.tableName = tableName;
+
+            return this;
+        }
+
+        /**
+         * Sets the valid table column names to prevent SQL injection.
+         *
+         * @param columnNames Acceptable columns names. Must be unquoted name 
or name is cast to upper case.
+         * @return This builder instance.
+         */
+        public SqlSerializer.Builder columns(Collection<String> columnNames) {
+            this.columnNames = columnNames;
+
+            return this;
+        }
+
+        /**
+         * Set the given criteria.
+         *
+         * @param where The predicate to filter entries or {@code null} to 
return all entries from the underlying table.
+         */
+        public SqlSerializer.Builder where(@Nullable Criteria where) {
+            this.where = where;
+
+            return this;
+        }
+
+        /**
+         * Builds the SQL query.
+         *
+         * @return SQL query text and arguments.
+         */
+        public SqlSerializer build() {
+            if (nullOrBlank(tableName)) {
+                throw new IllegalArgumentException("Table name can't be null 
or blank");
+            }
+
+            SqlSerializer ser = new SqlSerializer()
+                    .append("SELECT * ")
+                    .append("FROM ").append(quoteIfNeeded(tableName));
+
+            if (where != null) {
+                if (CollectionUtils.nullOrEmpty(columnNames)) {
+                    throw new IllegalArgumentException("The columns of the 
table must be specified to validate input");
+                }
+
+                ColumnValidator.INSTANCE.visit(where, columnNames);
+
+                ser.append(" WHERE ");
+                ser.visit(where, null);
+            }
+
+            return ser;
+        }
+    }
+}
diff --git 
a/modules/runner/src/testFixtures/java/org/apache/ignite/internal/ClusterPerClassIntegrationTest.java
 
b/modules/runner/src/testFixtures/java/org/apache/ignite/internal/ClusterPerClassIntegrationTest.java
index ff87690d65..c5772aad6b 100644
--- 
a/modules/runner/src/testFixtures/java/org/apache/ignite/internal/ClusterPerClassIntegrationTest.java
+++ 
b/modules/runner/src/testFixtures/java/org/apache/ignite/internal/ClusterPerClassIntegrationTest.java
@@ -353,38 +353,17 @@ public abstract class ClusterPerClassIntegrationTest 
extends IgniteIntegrationTe
      * {@link #deletePeople(String, int...)} to remove people.
      */
     protected static class Person {
-        int id;
+        final int id;
 
-        String name;
+        final String name;
 
-        double salary;
-
-        public Person() {
-            //No-op.
-        }
+        final double salary;
 
         public Person(int id, String name, double salary) {
             this.id = id;
             this.name = name;
             this.salary = salary;
         }
-
-        @Override
-        public boolean equals(Object o) {
-            if (this == o) {
-                return true;
-            }
-            if (o == null || getClass() != o.getClass()) {
-                return false;
-            }
-            Person person = (Person) o;
-            return id == person.id && Double.compare(salary, person.salary) == 
0 && Objects.equals(name, person.name);
-        }
-
-        @Override
-        public int hashCode() {
-            return Objects.hash(id, name, salary);
-        }
     }
 
     /**
diff --git 
a/modules/table/src/integrationTest/java/org/apache/ignite/internal/table/ItCriteriaQueryTest.java
 
b/modules/table/src/integrationTest/java/org/apache/ignite/internal/table/ItCriteriaQueryTest.java
index 61ac09b90d..be646bc4a3 100644
--- 
a/modules/table/src/integrationTest/java/org/apache/ignite/internal/table/ItCriteriaQueryTest.java
+++ 
b/modules/table/src/integrationTest/java/org/apache/ignite/internal/table/ItCriteriaQueryTest.java
@@ -17,26 +17,53 @@
 
 package org.apache.ignite.internal.table;
 
+import static java.util.Spliterators.spliteratorUnknownSize;
+import static java.util.function.Function.identity;
+import static java.util.stream.Collectors.toList;
+import static org.apache.ignite.internal.lang.IgniteStringFormatter.format;
 import static org.apache.ignite.internal.testframework.IgniteTestUtils.await;
 import static 
org.apache.ignite.internal.testframework.matchers.TupleMatcher.tupleValue;
+import static org.apache.ignite.lang.util.IgniteNameUtils.quote;
+import static org.apache.ignite.table.criteria.Criteria.columnValue;
+import static org.apache.ignite.table.criteria.Criteria.equalTo;
+import static org.apache.ignite.table.criteria.Criteria.greaterThan;
+import static org.apache.ignite.table.criteria.Criteria.greaterThanOrEqualTo;
+import static org.apache.ignite.table.criteria.Criteria.in;
+import static org.apache.ignite.table.criteria.Criteria.lessThan;
+import static org.apache.ignite.table.criteria.Criteria.lessThanOrEqualTo;
+import static org.apache.ignite.table.criteria.Criteria.notEqualTo;
+import static org.apache.ignite.table.criteria.Criteria.notIn;
+import static org.apache.ignite.table.criteria.Criteria.notNullValue;
+import static org.apache.ignite.table.criteria.Criteria.nullValue;
 import static org.apache.ignite.table.criteria.CriteriaQueryOptions.builder;
-import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.allOf;
 import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.is;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 
-import com.google.common.collect.Lists;
+import java.util.List;
+import java.util.Map;
+import java.util.Spliterator;
+import java.util.function.Function;
 import java.util.stream.Stream;
-import org.apache.ignite.Ignite;
+import java.util.stream.StreamSupport;
 import org.apache.ignite.client.IgniteClient;
 import org.apache.ignite.internal.ClusterPerClassIntegrationTest;
+import org.apache.ignite.internal.testframework.IgniteTestUtils;
 import org.apache.ignite.internal.util.IgniteUtils;
 import org.apache.ignite.lang.AsyncCursor;
 import org.apache.ignite.lang.Cursor;
+import org.apache.ignite.lang.IgniteException;
 import org.apache.ignite.table.RecordView;
+import org.apache.ignite.table.Table;
 import org.apache.ignite.table.Tuple;
+import org.apache.ignite.table.criteria.CriteriaQuerySource;
+import org.apache.ignite.table.mapper.Mapper;
+import org.hamcrest.Matcher;
+import org.hamcrest.Matchers;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
@@ -49,6 +76,12 @@ import org.junit.jupiter.params.provider.MethodSource;
  * Tests for the criteria query API.
  */
 public class ItCriteriaQueryTest extends ClusterPerClassIntegrationTest {
+    /** Table name. */
+    private static final String TABLE_NAME = "tbl";
+
+    /** Table with quoted name. */
+    private static final String QUOTED_TABLE_NAME = quote("TaBleName");
+
     private static IgniteClient CLIENT;
 
     /** {@inheritDoc} */
@@ -66,11 +99,25 @@ public class ItCriteriaQueryTest extends 
ClusterPerClassIntegrationTest {
         CLIENT = IgniteClient.builder()
                 .addresses("127.0.0.1:" + 
CLUSTER.aliveNode().clientAddress().port()).build();
 
-        createTable(DEFAULT_TABLE_NAME, 1, 8);
+        sql(format("CREATE TABLE {} (id INT PRIMARY KEY, name VARCHAR, salary 
DOUBLE, hash VARBINARY)", TABLE_NAME));
 
-        for (int i = 0; i < 3; i++) {
-            insertPeople(DEFAULT_TABLE_NAME, new Person(i, "name" + i, 10.0d * 
i));
-        }
+        insertData(
+                TABLE_NAME,
+                List.of("ID", "name", "salary", "hash"),
+                new Object[]{0, null, 0.0d, "hash0".getBytes()},
+                new Object[]{1, "name1", 10.0d, "hash1".getBytes()},
+                new Object[]{2, "name2", 20.0d, "hash2".getBytes()}
+        );
+
+        sql(format("CREATE TABLE {} (id INT PRIMARY KEY, \"colUmn\" VARCHAR)", 
QUOTED_TABLE_NAME));
+
+        insertData(
+                QUOTED_TABLE_NAME,
+                List.of("id", quote("colUmn")),
+                new Object[]{0, "name0"},
+                new Object[]{1, "name1"},
+                new Object[]{2, "name2"}
+        );
     }
 
     @AfterAll
@@ -78,57 +125,160 @@ public class ItCriteriaQueryTest extends 
ClusterPerClassIntegrationTest {
         IgniteUtils.closeAll(CLIENT);
     }
 
-    private static Stream<Arguments> testRecordBinaryView() {
+    private static Stream<Arguments> testRecordViewQuery() {
+        Table table = CLUSTER.aliveNode().tables().table(TABLE_NAME);
+        Table clientTable = CLIENT.tables().table(TABLE_NAME);
+
         return Stream.of(
-                Arguments.of(CLIENT),
-                Arguments.of(CLUSTER.aliveNode())
+                Arguments.of(table.recordView(), identity()),
+                Arguments.of(clientTable.recordView(), identity()),
+                Arguments.of(clientTable.recordView(TestObject.class),
+                        (Function<TestObject, Tuple>) (obj) -> 
Tuple.create().set("id", obj.id).set("name", obj.name)
+                                .set("salary", obj.salary).set("hash", 
obj.hash))
         );
     }
 
-    @ParameterizedTest(autoCloseArguments = false)
+    @ParameterizedTest
     @MethodSource
-    public void testRecordBinaryView(Ignite ignite) {
-        RecordView<Tuple> view = 
ignite.tables().table(DEFAULT_TABLE_NAME).recordView();
-
-        try (Cursor<Tuple> cur = view.query(null, null)) {
-            assertThat(Lists.newArrayList(cur), containsInAnyOrder(
-                    allOf(tupleValue("id", is(0)), tupleValue("name", 
is("name0")), tupleValue("salary", is(0.0d))),
-                    allOf(tupleValue("id", is(1)), tupleValue("name", 
is("name1")), tupleValue("salary", is(10.0d))),
-                    allOf(tupleValue("id", is(2)), tupleValue("name", 
is("name2")), tupleValue("salary", is(20.0d)))
-            ));
+    public <T> void testRecordViewQuery(CriteriaQuerySource<T> view, 
Function<T, Tuple> mapper) {
+        IgniteTestUtils.assertThrows(
+                IgniteException.class,
+                () -> view.query(null, columnValue("id", equalTo("2"))),
+                "Dynamic parameter requires adding explicit type cast"
+        );
+
+        Matcher<Tuple> person0 = allOf(tupleValue("id", is(0)), 
tupleValue("name", Matchers.nullValue()), tupleValue("salary", is(0.0d)),
+                tupleValue("hash", is("hash0".getBytes())));
+        Matcher<Tuple> person1 = allOf(tupleValue("id", is(1)), 
tupleValue("name", is("name1")), tupleValue("salary", is(10.0d)),
+                tupleValue("hash", is("hash1".getBytes())));
+        Matcher<Tuple> person2 = allOf(tupleValue("id", is(2)), 
tupleValue("name", is("name2")), tupleValue("salary", is(20.0d)),
+                tupleValue("hash", is("hash2".getBytes())));
+
+        try (Cursor<T> cur = view.query(null, null)) {
+            assertThat(mapToTupleList(cur, mapper), 
containsInAnyOrder(person0, person1, person2));
+        }
+
+        try (Cursor<T> cur = view.query(null, columnValue("id", equalTo(2)))) {
+            assertThat(mapToTupleList(cur, mapper), 
containsInAnyOrder(person2));
+        }
+
+        try (Cursor<T> cur = view.query(null, columnValue("hash", 
equalTo("hash2".getBytes())))) {
+            assertThat(mapToTupleList(cur, mapper), 
containsInAnyOrder(person2));
         }
+
+        try (Cursor<T> cur = view.query(null, columnValue("id", 
notEqualTo(2)))) {
+            assertThat(mapToTupleList(cur, mapper), 
containsInAnyOrder(person0, person1));
+        }
+
+        try (Cursor<T> cur = view.query(null, columnValue("hash", 
notEqualTo("hash2".getBytes())))) {
+            assertThat(mapToTupleList(cur, mapper), 
containsInAnyOrder(person0, person1));
+        }
+
+        try (Cursor<T> cur = view.query(null, columnValue("id", 
greaterThan(1)))) {
+            assertThat(mapToTupleList(cur, mapper), 
containsInAnyOrder(person2));
+        }
+
+        try (Cursor<T> cur = view.query(null, columnValue("id", 
greaterThanOrEqualTo(1)))) {
+            assertThat(mapToTupleList(cur, mapper), 
containsInAnyOrder(person1, person2));
+        }
+
+        try (Cursor<T> cur = view.query(null, columnValue("id", lessThan(1)))) 
{
+            assertThat(mapToTupleList(cur, mapper), 
containsInAnyOrder(person0));
+        }
+
+        try (Cursor<T> cur = view.query(null, columnValue("id", 
lessThanOrEqualTo(1)))) {
+            assertThat(mapToTupleList(cur, mapper), 
containsInAnyOrder(person0, person1));
+        }
+
+        try (Cursor<T> cur = view.query(null, columnValue("name", 
nullValue()))) {
+            assertThat(mapToTupleList(cur, mapper), 
containsInAnyOrder(person0));
+        }
+
+        try (Cursor<T> cur = view.query(null, columnValue("name", 
notNullValue()))) {
+            assertThat(mapToTupleList(cur, mapper), 
containsInAnyOrder(person1, person2));
+        }
+
+        try (Cursor<T> cur = view.query(null, columnValue("id", in(1, 2)))) {
+            assertThat(mapToTupleList(cur, mapper), 
containsInAnyOrder(person1, person2));
+        }
+
+        try (Cursor<T> cur = view.query(null, columnValue("id", notIn(1, 2)))) 
{
+            assertThat(mapToTupleList(cur, mapper), 
containsInAnyOrder(person0));
+        }
+
+        try (Cursor<T> cur = view.query(null, columnValue("hash", 
in("hash1".getBytes(), "hash2".getBytes())))) {
+            assertThat(mapToTupleList(cur, mapper), 
containsInAnyOrder(person1, person2));
+        }
+
+        try (Cursor<T> cur = view.query(null, columnValue("hash", in((byte[]) 
null)))) {
+            assertThat(mapToTupleList(cur, mapper), empty());
+        }
+
+        try (Cursor<T> cur = view.query(null, columnValue("hash", 
notIn("hash1".getBytes(), "hash2".getBytes())))) {
+            assertThat(mapToTupleList(cur, mapper), 
containsInAnyOrder(person0));
+        }
+
+        try (Cursor<T> cur = view.query(null, columnValue("hash", 
notIn((byte[]) null)))) {
+            assertThat(mapToTupleList(cur, mapper), 
containsInAnyOrder(person0, person1, person2));
+        }
+    }
+
+    @Test
+    public void testOptions() {
+        RecordView<TestObject> view = 
CLIENT.tables().table(TABLE_NAME).recordView(TestObject.class);
+
+        AsyncCursor<TestObject> ars = await(view.queryAsync(null, null, 
builder().pageSize(2).build()));
+
+        assertNotNull(ars);
+        assertEquals(2, ars.currentPageSize());
+        await(ars.closeAsync());
     }
 
-    private static Stream<Arguments> testRecordPojoView() {
+    private static Stream<Arguments> testRecordViewWithQuotes() {
+        Table table = CLUSTER.aliveNode().tables().table(QUOTED_TABLE_NAME);
+        Table clientTable = CLIENT.tables().table(QUOTED_TABLE_NAME);
+
+        Mapper<QuotedObject> pojoMapper = Mapper.builder(QuotedObject.class)
+                .map("colUmn", quote("colUmn"))
+                .automap()
+                .build();
+
         return Stream.of(
-                // TODO https://issues.apache.org/jira/browse/IGNITE-20977
-                //Arguments.of(CLUSTER.aliveNode()),
-                Arguments.of(CLIENT)
+                Arguments.of(table.recordView(), identity()),
+                Arguments.of(clientTable.recordView(), identity()),
+                Arguments.of(clientTable.recordView(pojoMapper),
+                        (Function<QuotedObject, Tuple>) (obj) -> 
Tuple.create(Map.of("id", obj.id, quote("colUmn"), obj.colUmn)))
         );
     }
 
-    @ParameterizedTest(autoCloseArguments = false)
+    @ParameterizedTest
     @MethodSource
-    public void testRecordPojoView(Ignite ignite) {
-        RecordView<Person> view = 
ignite.tables().table(DEFAULT_TABLE_NAME).recordView(Person.class);
-
-        try (Cursor<Person> cur = view.query(null, null)) {
-            assertThat(Lists.newArrayList(cur), containsInAnyOrder(
-                    new Person(0, "name0", 0.0d),
-                    new Person(1, "name1", 10.0d),
-                    new Person(2, "name2", 20.0d)
+    public <T> void testRecordViewWithQuotes(CriteriaQuerySource<T> view, 
Function<T, Tuple> mapper) {
+        try (Cursor<T> cur = view.query(null, columnValue(quote("colUmn"), 
equalTo("name1")))) {
+            assertThat(mapToTupleList(cur, mapper), containsInAnyOrder(
+                    allOf(tupleValue("id", is(1)), tupleValue(quote("colUmn"), 
is("name1")))
             ));
         }
     }
 
-    @Test
-    public void testOptions() {
-        RecordView<Person> view = 
CLIENT.tables().table(DEFAULT_TABLE_NAME).recordView(Person.class);
+    private static <T> List<Tuple> mapToTupleList(Cursor<T> cur, Function<T, 
Tuple> mapper) {
+        return StreamSupport.stream(spliteratorUnknownSize(cur, 
Spliterator.ORDERED), false)
+                .map(mapper)
+                .collect(toList());
+    }
 
-        AsyncCursor<Person> ars = await(view.queryAsync(null, null, 
builder().pageSize(2).build()));
+    static class QuotedObject {
+        int id;
+        String colUmn;
+    }
 
-        assertNotNull(ars);
-        assertEquals(2, ars.currentPageSize());
-        await(ars.closeAsync());
+    static class TestObject {
+        int id;
+
+        String name;
+
+        double salary;
+
+        byte[] hash;
     }
 }
diff --git 
a/modules/table/src/main/java/org/apache/ignite/internal/table/AbstractTableView.java
 
b/modules/table/src/main/java/org/apache/ignite/internal/table/AbstractTableView.java
index 6de0d37fbd..01406d72c9 100644
--- 
a/modules/table/src/main/java/org/apache/ignite/internal/table/AbstractTableView.java
+++ 
b/modules/table/src/main/java/org/apache/ignite/internal/table/AbstractTableView.java
@@ -22,16 +22,19 @@ import static java.util.function.Function.identity;
 import static 
org.apache.ignite.internal.lang.IgniteExceptionMapperUtil.convertToPublicFuture;
 import static org.apache.ignite.internal.util.ExceptionUtils.sneakyThrow;
 
+import java.util.Collection;
 import java.util.Objects;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.ExecutionException;
 import org.apache.ignite.internal.lang.IgniteExceptionMapperUtil;
 import org.apache.ignite.internal.schema.SchemaRegistry;
+import org.apache.ignite.internal.table.criteria.SqlSerializer;
 import 
org.apache.ignite.internal.table.distributed.replicator.InternalSchemaVersionMismatchException;
 import org.apache.ignite.internal.table.distributed.schema.SchemaVersions;
 import org.apache.ignite.internal.tx.InternalTransaction;
 import org.apache.ignite.internal.util.ExceptionUtils;
 import org.apache.ignite.sql.IgniteSql;
+import org.apache.ignite.table.criteria.Criteria;
 import org.apache.ignite.tx.Transaction;
 import org.jetbrains.annotations.Nullable;
 
@@ -137,6 +140,22 @@ abstract class AbstractTableView {
         return ex != null && (exceptionClass.isInstance(ex) || 
isOrCausedBy(exceptionClass, ex.getCause()));
     }
 
+    /**
+     * Construct SQL query and arguments for prepare statement.
+     *
+     * @param tableName Table name.
+     * @param columnNames Column names.
+     * @param criteria The predicate to filter entries or {@code null} to 
return all entries from the underlying table.
+     * @return SQL query and it's arguments.
+     */
+    static SqlSerializer createSqlSerializer(String tableName, 
Collection<String> columnNames, @Nullable Criteria criteria) {
+        return new SqlSerializer.Builder()
+                .tableName(tableName)
+                .columns(columnNames)
+                .where(criteria)
+                .build();
+    }
+
     /**
      * Action representing some KV operation. When executed, the action is 
supplied with schema version corresponding
      * to the operation timestamp (see {@link #withSchemaSync(Transaction, 
KvAction)} for details).
diff --git 
a/modules/table/src/main/java/org/apache/ignite/internal/table/RecordBinaryViewImpl.java
 
b/modules/table/src/main/java/org/apache/ignite/internal/table/RecordBinaryViewImpl.java
index 247ee7d45d..12368105c9 100644
--- 
a/modules/table/src/main/java/org/apache/ignite/internal/table/RecordBinaryViewImpl.java
+++ 
b/modules/table/src/main/java/org/apache/ignite/internal/table/RecordBinaryViewImpl.java
@@ -26,6 +26,7 @@ import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.Flow.Publisher;
 import org.apache.ignite.internal.schema.BinaryRow;
 import org.apache.ignite.internal.schema.BinaryRowEx;
+import org.apache.ignite.internal.schema.SchemaDescriptor;
 import org.apache.ignite.internal.schema.SchemaRegistry;
 import org.apache.ignite.internal.schema.marshaller.TupleMarshaller;
 import org.apache.ignite.internal.schema.marshaller.TupleMarshallerException;
@@ -33,6 +34,7 @@ import org.apache.ignite.internal.schema.row.Row;
 import org.apache.ignite.internal.streamer.StreamerBatchSender;
 import org.apache.ignite.internal.table.criteria.CursorAdapter;
 import org.apache.ignite.internal.table.criteria.QueryCriteriaAsyncCursor;
+import org.apache.ignite.internal.table.criteria.SqlSerializer;
 import org.apache.ignite.internal.table.distributed.schema.SchemaVersions;
 import org.apache.ignite.internal.tx.InternalTransaction;
 import org.apache.ignite.lang.AsyncCursor;
@@ -458,14 +460,17 @@ public class RecordBinaryViewImpl extends 
AbstractTableView implements RecordVie
             @Nullable Criteria criteria,
             @Nullable CriteriaQueryOptions opts
     ) {
-        //TODO: implement serialization of criteria to SQL 
https://issues.apache.org/jira/browse/IGNITE-20879
-        var query = "SELECT * FROM " + tbl.name();
         var opts0 = opts == null ? CriteriaQueryOptions.DEFAULT : opts;
 
-        Statement statement = 
sql.statementBuilder().query(query).pageSize(opts0.pageSize()).build();
-        Session session = sql.createSession();
+        return withSchemaSync(tx, (schemaVersion) -> {
+            SchemaDescriptor schema = 
rowConverter.registry().schema(schemaVersion);
+            SqlSerializer ser = createSqlSerializer(tbl.name(), 
schema.columnNames(), criteria);
+
+            Statement statement = 
sql.statementBuilder().query(ser.toString()).pageSize(opts0.pageSize()).build();
+            Session session = sql.createSession();
 
-        return session.executeAsync(tx, statement)
-                .thenApply(resultSet -> new 
QueryCriteriaAsyncCursor<>(resultSet, session::close));
+            return session.executeAsync(tx, statement, ser.getArguments())
+                    .thenApply(resultSet -> new 
QueryCriteriaAsyncCursor<>(resultSet, session::close));
+        });
     }
 }
diff --git 
a/modules/table/src/main/java/org/apache/ignite/internal/table/RecordViewImpl.java
 
b/modules/table/src/main/java/org/apache/ignite/internal/table/RecordViewImpl.java
index 4dcba9fb50..4b6e8b0ed6 100644
--- 
a/modules/table/src/main/java/org/apache/ignite/internal/table/RecordViewImpl.java
+++ 
b/modules/table/src/main/java/org/apache/ignite/internal/table/RecordViewImpl.java
@@ -35,6 +35,7 @@ import org.apache.ignite.internal.schema.row.Row;
 import org.apache.ignite.internal.streamer.StreamerBatchSender;
 import org.apache.ignite.internal.table.criteria.CursorAdapter;
 import org.apache.ignite.internal.table.criteria.QueryCriteriaAsyncCursor;
+import org.apache.ignite.internal.table.criteria.SqlSerializer;
 import org.apache.ignite.internal.table.distributed.schema.SchemaVersions;
 import org.apache.ignite.internal.tx.InternalTransaction;
 import org.apache.ignite.lang.AsyncCursor;
@@ -551,14 +552,17 @@ public class RecordViewImpl<R> extends AbstractTableView 
implements RecordView<R
             @Nullable Criteria criteria,
             @Nullable CriteriaQueryOptions opts
     ) {
-        //TODO: implement serialization of criteria to SQL 
https://issues.apache.org/jira/browse/IGNITE-20879
-        var query = "SELECT * FROM " + tbl.name();
         var opts0 = opts == null ? CriteriaQueryOptions.DEFAULT : opts;
 
-        Statement statement = 
sql.statementBuilder().query(query).pageSize(opts0.pageSize()).build();
-        Session session = sql.createSession();
+        return withSchemaSync(tx, (schemaVersion) -> {
+            SchemaDescriptor schema = 
rowConverter.registry().schema(schemaVersion);
+            SqlSerializer ser = createSqlSerializer(tbl.name(), 
schema.columnNames(), criteria);
+
+            Statement statement = 
sql.statementBuilder().query(ser.toString()).pageSize(opts0.pageSize()).build();
+            Session session = sql.createSession();
 
-        return session.executeAsync(tx, mapper, statement)
-                .thenApply(resultSet -> new 
QueryCriteriaAsyncCursor<>(resultSet, session::close));
+            return session.executeAsync(tx, mapper, statement, 
ser.getArguments())
+                    .thenApply(resultSet -> new 
QueryCriteriaAsyncCursor<>(resultSet, session::close));
+        });
     }
 }
diff --git 
a/modules/table/src/test/java/org/apache/ignite/internal/table/criteria/SqlSerializerTest.java
 
b/modules/table/src/test/java/org/apache/ignite/internal/table/criteria/SqlSerializerTest.java
new file mode 100644
index 0000000000..151052ef05
--- /dev/null
+++ 
b/modules/table/src/test/java/org/apache/ignite/internal/table/criteria/SqlSerializerTest.java
@@ -0,0 +1,208 @@
+/*
+ * 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.ignite.internal.table.criteria;
+
+import static org.apache.ignite.internal.lang.IgniteStringFormatter.format;
+import static org.apache.ignite.internal.util.ArrayUtils.OBJECT_EMPTY_ARRAY;
+import static org.apache.ignite.lang.util.IgniteNameUtils.quote;
+import static org.apache.ignite.table.criteria.Criteria.and;
+import static org.apache.ignite.table.criteria.Criteria.columnValue;
+import static org.apache.ignite.table.criteria.Criteria.equalTo;
+import static org.apache.ignite.table.criteria.Criteria.greaterThan;
+import static org.apache.ignite.table.criteria.Criteria.greaterThanOrEqualTo;
+import static org.apache.ignite.table.criteria.Criteria.in;
+import static org.apache.ignite.table.criteria.Criteria.lessThan;
+import static org.apache.ignite.table.criteria.Criteria.lessThanOrEqualTo;
+import static org.apache.ignite.table.criteria.Criteria.not;
+import static org.apache.ignite.table.criteria.Criteria.notEqualTo;
+import static org.apache.ignite.table.criteria.Criteria.notIn;
+import static org.apache.ignite.table.criteria.Criteria.notNullValue;
+import static org.apache.ignite.table.criteria.Criteria.nullValue;
+import static org.apache.ignite.table.criteria.Criteria.or;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.endsWith;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.params.provider.Arguments.of;
+
+import java.util.Set;
+import java.util.function.Consumer;
+import java.util.stream.Stream;
+import org.apache.ignite.table.criteria.Criteria;
+import org.apache.ignite.table.criteria.Expression;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+/**
+ * SQL generation test.
+ */
+class SqlSerializerTest {
+    static Stream<Arguments> testCondition() {
+        byte[] arr1 = "arr1".getBytes();
+        byte[] arr2 = "arr2".getBytes();
+
+        return Stream.of(
+                of(columnValue("a", equalTo("a1")), "A = ?", new 
Object[]{"a1"}),
+                of(columnValue("a", equalTo(arr1)), "A = ?", new 
Object[]{arr1}),
+                of(columnValue("a", equalTo((Comparable<Object>) null)), "A IS 
NULL", OBJECT_EMPTY_ARRAY),
+                of(columnValue("a", equalTo((byte[]) null)), "A IS NULL", 
OBJECT_EMPTY_ARRAY),
+                of(columnValue("a", notEqualTo("a1")), "A <> ?", new 
Object[]{"a1"}),
+                of(columnValue("a", notEqualTo(arr1)), "A <> ?", new 
Object[]{arr1}),
+                of(columnValue("a", notEqualTo((Comparable<Object>) null)), "A 
IS NOT NULL", OBJECT_EMPTY_ARRAY),
+                of(columnValue("a", notEqualTo((byte[]) null)), "A IS NOT 
NULL", OBJECT_EMPTY_ARRAY),
+                of(columnValue("a", greaterThan("a1")), "A > ?", new 
Object[]{"a1"}),
+                of(columnValue("a", greaterThanOrEqualTo("a1")), "A >= ?", new 
Object[]{"a1"}),
+                of(columnValue("a", lessThan("a1")), "A < ?", new 
Object[]{"a1"}),
+                of(columnValue("a", lessThanOrEqualTo("a1")), "A <= ?", new 
Object[]{"a1"}),
+                of(columnValue("a", nullValue()), "A IS NULL", 
OBJECT_EMPTY_ARRAY),
+                of(columnValue("a", notNullValue()), "A IS NOT NULL", 
OBJECT_EMPTY_ARRAY),
+                of(columnValue("a", in("a1", "a2")), "A IN (?, ?)", new 
Object[]{"a1", "a2"}),
+                of(columnValue("a", in("a1")), "A = ?", new Object[]{"a1"}),
+                of(columnValue("a", in(arr1, arr2)), "A IN (?, ?)", new 
Object[]{arr1, arr2}),
+                of(columnValue("a", in(arr1)), "A = ?", new Object[]{arr1}),
+                of(columnValue("a", in((Comparable<Object>) null)), "A IS 
NULL", OBJECT_EMPTY_ARRAY),
+                of(columnValue("a", in((byte[]) null)), "A IS NULL", 
OBJECT_EMPTY_ARRAY),
+                of(columnValue("a", notIn("a1", "a2")), "A NOT IN (?, ?)", new 
Object[]{"a1", "a2"}),
+                of(columnValue("a", notIn("a1")), "A <> ?", new 
Object[]{"a1"}),
+                of(columnValue("a", notIn("a1")), "A <> ?", new 
Object[]{"a1"}),
+                of(columnValue("a", notIn((Comparable<Object>) null)), "A IS 
NOT NULL", OBJECT_EMPTY_ARRAY),
+                of(columnValue("a", notIn((byte[]) null)), "A IS NOT NULL", 
OBJECT_EMPTY_ARRAY)
+        );
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testCondition(Criteria criteria, String wherePart, Object[] 
arguments) {
+        SqlSerializer ser = new SqlSerializer.Builder()
+                .tableName("test")
+                .columns(Set.of("A"))
+                .where(criteria)
+                .build();
+
+        assertThat(ser.toString(), endsWith(wherePart));
+        assertArrayEquals(arguments, ser.getArguments());
+    }
+
+    @Test
+    void testInvalidCondition() {
+        assertThrows(IllegalArgumentException.class, () -> in(new 
Comparable[0]), "values must not be empty or null");
+        assertThrows(IllegalArgumentException.class, () -> in(new byte[0][0]), 
"values must not be empty or null");
+        assertThrows(IllegalArgumentException.class, () -> notIn(new 
Comparable[0]), "values must not be empty or null");
+        assertThrows(IllegalArgumentException.class, () -> notIn(new 
byte[0][0]), "values must not be empty or null");
+    }
+
+    static Stream<Arguments> testExpression() {
+        return Stream.of(
+                of(and(columnValue("a", nullValue())), "A IS NULL", 
OBJECT_EMPTY_ARRAY),
+                of(
+                        and(columnValue("a", nullValue()), columnValue("b", 
notIn("b1", "b2", "b3"))),
+                        "(A IS NULL) AND (B NOT IN (?, ?, ?))",
+                        new Object[] {"b1", "b2", "b3"}
+                ),
+                of(
+                        and(columnValue("a", nullValue()), columnValue("b", 
notIn("b1", "b2", "b3")), columnValue("c", equalTo("c1"))),
+                        "(A IS NULL) AND (B NOT IN (?, ?, ?)) AND (C = ?)",
+                        new Object[] {"b1", "b2", "b3", "c1"}
+                ),
+                of(or(columnValue("a", nullValue())), "A IS NULL", 
OBJECT_EMPTY_ARRAY),
+                of(
+                        or(columnValue("a", nullValue()), columnValue("b", 
notIn("b1", "b2", "b3"))),
+                        "(A IS NULL) OR (B NOT IN (?, ?, ?))",
+                        new Object[] {"b1", "b2", "b3"}
+                ),
+                of(
+                        or(columnValue("a", nullValue()), columnValue("b", 
notIn("b1", "b2", "b3")), columnValue("c", equalTo("c1"))),
+                        "(A IS NULL) OR (B NOT IN (?, ?, ?)) OR (C = ?)",
+                        new Object[] {"b1", "b2", "b3", "c1"}
+                ),
+                of(
+                        not(and(columnValue("a", equalTo("a1")), 
columnValue("b", equalTo("b1")))),
+                        "NOT ((A = ?) AND (B = ?))",
+                        new Object[] {"a1", "b1"}
+                )
+        );
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testExpression(Criteria criteria, String wherePart, Object[] 
arguments) {
+        SqlSerializer ser = new SqlSerializer.Builder()
+                .tableName("test")
+                .columns(Set.of("A", "B", "C"))
+                .where(criteria)
+                .build();
+
+        assertThat(ser.toString(), endsWith(wherePart));
+        assertArrayEquals(arguments, ser.getArguments());
+    }
+
+    static Stream<Arguments> testInvalidExpression() {
+        return Stream.<Consumer<Expression[]>>of(
+                Criteria::and,
+                Criteria::or
+        ).map(Arguments::of);
+    }
+
+    @ParameterizedTest
+    @MethodSource
+    void testInvalidExpression(Consumer<Expression[]> consumer) {
+        assertThrows(IllegalArgumentException.class, () -> 
consumer.accept(null), "expressions must not be empty or null");
+        assertThrows(IllegalArgumentException.class, () -> consumer.accept(new 
Expression[0]), "expressions must not be empty or null");
+        assertThrows(IllegalArgumentException.class, () -> consumer.accept(new 
Expression[]{columnValue("a", equalTo("a1")), null}),
+                "expressions must not be empty or null");
+    }
+
+    @Test
+    void testColumnNameValidation() {
+        IllegalArgumentException iae = assertThrows(
+                IllegalArgumentException.class,
+                () -> new SqlSerializer.Builder()
+                        .tableName("test")
+                        .where(columnValue("a", equalTo("a")))
+                        .build()
+        );
+
+        assertThat(iae.getMessage(), containsString("The columns of the table 
must be specified to validate input"));
+
+        iae = assertThrows(
+                IllegalArgumentException.class,
+                () -> new SqlSerializer.Builder()
+                        .tableName("test")
+                        .columns(Set.of("B"))
+                        .where(columnValue("a", equalTo("a")))
+                        .build()
+        );
+
+        assertThat(iae.getMessage(), containsString("Unexpected column name: 
A"));
+    }
+
+    @Test
+    void testQuote() {
+        SqlSerializer ser = new SqlSerializer.Builder()
+                .tableName("Test")
+                .columns(Set.of("Aa"))
+                .where(columnValue(quote("Aa"), equalTo(1)))
+                .build();
+
+        assertThat(ser.toString(), endsWith(format("FROM {} WHERE {} = ?", 
quote("Test"), quote("Aa"))));
+        assertArrayEquals(new Object[]{1}, ser.getArguments());
+    }
+}

Reply via email to