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

amashenkov 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 b676b539bf IGNITE-24036 Sql schema. Introduce QualifiedName class 
(#4971)
b676b539bf is described below

commit b676b539bfac6d9165a48a901a091c473b7576f1
Author: Andrew V. Mashenkov <[email protected]>
AuthorDate: Tue Jan 7 11:46:25 2025 +0300

    IGNITE-24036 Sql schema. Introduce QualifiedName class (#4971)
---
 .../apache/ignite/lang/util/IgniteNameUtils.java   |  32 +-
 .../org/apache/ignite/table/QualifiedName.java     | 365 +++++++++++++++++++++
 .../org/apache/ignite/table/QualifiedNameTest.java | 281 ++++++++++++++++
 .../org/apache/ignite/internal/sql/SqlCommon.java  |   5 +-
 4 files changed, 680 insertions(+), 3 deletions(-)

diff --git 
a/modules/api/src/main/java/org/apache/ignite/lang/util/IgniteNameUtils.java 
b/modules/api/src/main/java/org/apache/ignite/lang/util/IgniteNameUtils.java
index 7c7c3e13ad..411ef34507 100644
--- a/modules/api/src/main/java/org/apache/ignite/lang/util/IgniteNameUtils.java
+++ b/modules/api/src/main/java/org/apache/ignite/lang/util/IgniteNameUtils.java
@@ -24,6 +24,8 @@ import org.jetbrains.annotations.Nullable;
  * Utility methods used for cluster's named objects: schemas, tables, columns, 
indexes, etc.
  */
 public final class IgniteNameUtils {
+    // TODO https://issues.apache.org/jira/browse/IGNITE-24021 drop this.
+    @Deprecated
     private static final Pattern NAME_PATTER = 
Pattern.compile("^(?:\\p{Alpha}\\w*)(?:\\.\\p{Alpha}\\w*)?$");
 
     /** No instance methods. */
@@ -36,6 +38,8 @@ public final class IgniteNameUtils {
      * @param name String to parse object name.
      * @return Unquoted name or name is cast to upper case. "tbl0" -&gt; 
"TBL0", "\"Tbl0\"" -&gt; "Tbl0".
      */
+    // TODO https://issues.apache.org/jira/browse/IGNITE-24021: Use 
QualifiedName instead.
+    @Deprecated(forRemoval = true)
     public static String parseSimpleName(String name) {
         if (name == null || name.isEmpty()) {
             return name;
@@ -53,13 +57,14 @@ public final class IgniteNameUtils {
     }
 
     /**
-     * Creates a fully qualified name in canonical form, that is,
-     * enclosing each part of the identifier chain in double quotes.
+     * Creates a fully qualified name in canonical form, that is, enclosing 
each part of the identifier chain in double quotes.
      *
      * @param schemaName Name of the schema.
      * @param objectName Name of the object.
      * @return Returns fully qualified name in canonical form.
      */
+    // TODO https://issues.apache.org/jira/browse/IGNITE-24021: replace 
`quote` call with `quoteIfNeeded`
+    @Deprecated(forRemoval = true)
     public static String canonicalName(String schemaName, String objectName) {
         return quote(schemaName) + '.' + quote(objectName);
     }
@@ -70,6 +75,8 @@ public final class IgniteNameUtils {
      * @param s String to test.
      * @return {@code True} if given string is fully qualified name in 
canonical form or simple name.
      */
+    // TODO https://issues.apache.org/jira/browse/IGNITE-24021: drop this 
method.
+    @Deprecated(forRemoval = true)
     public static boolean canonicalOrSimpleName(String s) {
         return NAME_PATTER.matcher(s).matches();
     }
@@ -80,6 +87,8 @@ public final class IgniteNameUtils {
      * @param name Object name.
      * @return Quoted object name.
      */
+    // TODO https://issues.apache.org/jira/browse/IGNITE-24021 make it 
private, `quoteIfNeeded` should be used instead.
+    @Deprecated
     public static String quote(String name) {
         if (name == null || name.isEmpty()) {
             return name;
@@ -109,6 +118,8 @@ public final class IgniteNameUtils {
      * @param name Object name.
      * @return Quoted object name.
      */
+    // TODO https://issues.apache.org/jira/browse/IGNITE-24021 drop this 
method..
+    @Deprecated(forRemoval = true)
     public static String quoteIfNeeded(String name) {
         if (name == null || name.isEmpty()) {
             return null;
@@ -127,6 +138,22 @@ public final class IgniteNameUtils {
         return name.toUpperCase().equals(name) ? name : quote(name); // NOPMD
     }
 
+    /** An {@code identifier start} is any character in the Unicode General 
Category classes “Lu”, “Ll”, “Lt”, “Lm”, “Lo”, or “Nl”. */
+    public static boolean identifierStart(int codePoint) {
+        return Character.isAlphabetic(codePoint);
+    }
+
+    /** An {@code identifier extend} is U+00B7, or any character in the 
Unicode General Category classes “Mn”, “Mc”, “Nd”, “Pc”, or “Cf”.*/
+    public static boolean identifierExtend(int codePoint) {
+        return codePoint == ('·' & 0xff) /* “Middle Dot” character */
+                || ((((1 << Character.NON_SPACING_MARK)
+                | (1 << Character.COMBINING_SPACING_MARK)
+                | (1 << Character.DECIMAL_DIGIT_NUMBER)
+                | (1 << Character.CONNECTOR_PUNCTUATION)
+                | (1 << Character.FORMAT)) >> Character.getType(codePoint)) & 
1) != 0;
+
+    }
+
     /**
      * Identifier chain tokenizer.
      *
@@ -135,6 +162,7 @@ public final class IgniteNameUtils {
      * <p>This tokenizer is not SQL compliant, but it is ok since it used to 
retrieve an object only. The sole purpose of this tokenizer
      * is to split the chain into parts by a dot considering the quotation.
      */
+    // TODO https://issues.apache.org/jira/browse/IGNITE-24021 Replace this 
with tokenizer from QualifiedName.
     private static class Tokenizer {
         private int currentPosition;
         private final String source;
diff --git 
a/modules/api/src/main/java/org/apache/ignite/table/QualifiedName.java 
b/modules/api/src/main/java/org/apache/ignite/table/QualifiedName.java
new file mode 100644
index 0000000000..6ab460a0c3
--- /dev/null
+++ b/modules/api/src/main/java/org/apache/ignite/table/QualifiedName.java
@@ -0,0 +1,365 @@
+/*
+ * 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;
+
+import static org.apache.ignite.lang.util.IgniteNameUtils.identifierExtend;
+import static org.apache.ignite.lang.util.IgniteNameUtils.identifierStart;
+import static org.apache.ignite.lang.util.IgniteNameUtils.quote;
+
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Class represents a catalog object name (table, index and etc.) and provides 
factory methods.
+ *
+ * <p>Qualified name is a pair of schema name and object name.
+ *
+ * <p>Factory methods expects that given names (both: schema name and object 
name) respect SQL syntax rules for identifiers.
+ * <ul>
+ * <li>Identifier must starts from any character in the Unicode General 
Category classes “Lu”, “Ll”, “Lt”, “Lm”, “Lo”, or “Nl”.
+ * <li>Identifier character (expect the first one) may be U+00B7 (middle dot), 
or any character in the Unicode General Category classes
+ * “Mn”, “Mc”, “Nd”, “Pc”, or “Cf”.
+ * <li>Identifier that contains any other characters must be quoted with 
double-quotes.
+ * <li>Double-quote inside the identifier must be encoded as 2 consequent 
double-quote chars.
+ * </ul>
+ *
+ * <p>{@link QualifiedName#parse(String)} method also accepts a qualified name 
in canonical form: {@code [<schema_name>.]<object_name>}.
+ * Schema name is optional, when not set, then {@link 
QualifiedName#DEFAULT_SCHEMA_NAME} will be used.
+ *
+ * <p>The object contains normalized names, which are case sensitive. That 
means, an unquoted name will be cast to upper case;
+ * for quoted names - the unnecessary quotes will be removed preserving 
escaped double-quote symbols.
+ * E.g. "tbl0" - is equivalent to "TBL0", "\"Tbl0\"" - "Tbl0", etc.
+ */
+public class QualifiedName {
+    /** Default schema name. */
+    public static final String DEFAULT_SCHEMA_NAME = "PUBLIC";
+
+    /** Normalized schema name. */
+    private final String schemaIdentifier;
+
+    /** Normalized object name. */
+    private final String objectIdentifier;
+
+    /**
+     * Factory method that creates qualified name object by parsing given 
simple or canonical object name.
+     *
+     * @param simpleOrCanonicalName Object simple name or qualified name in 
canonical form.
+     * @return Qualified name.
+     */
+    public static QualifiedName parse(String simpleOrCanonicalName) {
+        verifyObjectIdentifier(simpleOrCanonicalName);
+
+        Tokenizer tokenizer = new Tokenizer(simpleOrCanonicalName);
+
+        String schemaName = DEFAULT_SCHEMA_NAME;
+        String objectName = tokenizer.nextToken();
+
+        if (tokenizer.hasNext()) {
+            // Canonical name
+            schemaName = objectName;
+            objectName = tokenizer.nextToken();
+        }
+
+        if (tokenizer.hasNext()) {
+            throw new IllegalArgumentException("Canonical name format 
mismatch: " + simpleOrCanonicalName);
+        }
+
+        verifySchemaIdentifier(schemaName);
+        verifyObjectIdentifier(objectName);
+
+        return new QualifiedName(schemaName, objectName);
+    }
+
+    /**
+     * Factory method that creates qualified name from given simple name by 
resolving it against the default schema.
+     *
+     * @param simpleName Object name.
+     * @return Qualified name.
+     */
+    public static QualifiedName fromSimple(String simpleName) {
+        return of(null, simpleName);
+    }
+
+    /**
+     * Factory method that creates qualified name from given schema and object 
name.
+     *
+     * @param schemaName Schema name or {@code null} for default schema.
+     * @param objectName Object name.
+     * @return Qualified name.
+     */
+    public static QualifiedName of(@Nullable String schemaName, String 
objectName) {
+        String schemaIdentifier = schemaName == null ? DEFAULT_SCHEMA_NAME : 
parseIdentifier(schemaName);
+        String objectIdentifier = parseIdentifier(objectName);
+
+        verifySchemaIdentifier(schemaIdentifier);
+        verifyObjectIdentifier(objectIdentifier);
+
+        return new QualifiedName(schemaIdentifier, objectIdentifier);
+    }
+
+    /**
+     * Constructs a qualified name.
+     *
+     * @param schemaName Normalized schema name.
+     * @param objectName Normalized object name.
+     */
+    private QualifiedName(String schemaName, String objectName) {
+        this.schemaIdentifier = schemaName;
+        this.objectIdentifier = objectName;
+    }
+
+    /** Returns normalized schema name. */
+    public String schemaName() {
+        return schemaIdentifier;
+    }
+
+    /** Returns normalized object name. */
+    public String objectName() {
+        return objectIdentifier;
+    }
+
+    /** Returns qualified name in canonical form. */
+    public String toCanonicalForm() {
+        // TODO https://issues.apache.org/jira/browse/IGNITE-24021 Extract 
method and move to IgniteNameUtils
+        return quoteIfNeeded(schemaIdentifier) + '.' + 
quoteIfNeeded(objectIdentifier);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public boolean equals(Object object) {
+        if (this == object) {
+            return true;
+        }
+        if (object == null || getClass() != object.getClass()) {
+            return false;
+        }
+        QualifiedName that = (QualifiedName) object;
+        return Objects.equals(schemaIdentifier, that.schemaIdentifier) && 
Objects.equals(objectIdentifier, that.objectIdentifier);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public int hashCode() {
+        return Objects.hash(schemaIdentifier, objectIdentifier);
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String toString() {
+        return "QualifiedName["
+                + "schemaName='" + schemaIdentifier + '\''
+                + ", objectName='" + objectIdentifier + '\''
+                + ']';
+    }
+
+    private static void verifyObjectIdentifier(@Nullable String identifier) {
+        Objects.requireNonNull(identifier);
+
+        if (identifier.isEmpty()) {
+            throw new IllegalArgumentException("Object identifier can't be 
empty.");
+        }
+    }
+
+    private static void verifySchemaIdentifier(@Nullable String identifier) {
+        if (identifier != null && identifier.isEmpty()) {
+            throw new IllegalArgumentException("Schema identifier can't be 
empty.");
+        }
+    }
+
+    /**
+     * Parse simple identifier.
+     *
+     * @param name Object name to parse.
+     * @return Unquoted case-sensitive name name or uppercased 
case-insensitive name.
+     * @see QualifiedName javadoc with syntax rules.
+     */
+    // TODO https://issues.apache.org/jira/browse/IGNITE-24021: Move to 
IgniteNameUtils and replace parseSimple(String).
+    private static String parseIdentifier(String name) {
+        if (name == null || name.isEmpty()) {
+            return name;
+        }
+
+        var tokenizer = new Tokenizer(name);
+
+        String parsedName = tokenizer.nextToken();
+
+        if (tokenizer.hasNext()) {
+            throw new IllegalArgumentException("Fully qualified name is not 
expected [name=" + name + "]");
+        }
+
+        return parsedName;
+    }
+
+    /**
+     * Wraps the given name with double quotes if it not uppercased non-quoted 
name, e.g. "myColumn" -&gt; "\"myColumn\"", "MYCOLUMN" -&gt;
+     * "MYCOLUMN"
+     *
+     * @param identifier Object identifier.
+     * @return Quoted object name.
+     */
+    // TODO https://issues.apache.org/jira/browse/IGNITE-24021: Move to 
IgniteNameUtils and replace current one.
+    private static String quoteIfNeeded(String identifier) {
+        if (identifier.isEmpty()) {
+            return identifier;
+        }
+
+        if (!identifierStart(identifier.codePointAt(0)) && 
!Character.isUpperCase(identifier.codePointAt(0))) {
+            return quote(identifier);
+        }
+
+        for (int pos = 1; pos < identifier.length(); pos++) {
+            int codePoint = identifier.codePointAt(pos);
+
+            if (!identifierExtend(codePoint) && 
!Character.isUpperCase(codePoint)) {
+                return quote(identifier);
+            }
+        }
+
+        return identifier;
+    }
+
+    /**
+     * Identifier chain tokenizer.
+     *
+     * <p>Splits provided identifier chain (complex identifier like 
PUBLIC.MY_TABLE) into its component parts.
+     */
+    // TODO https://issues.apache.org/jira/browse/IGNITE-24021: Move to 
IgniteNameUtils and replace parseSimple(String).
+    static class Tokenizer {
+        private final String source;
+        private int currentPosition;
+        private boolean foundDot;
+
+        /**
+         * Creates a tokenizer for given string source.
+         *
+         * @param source Source string to split.
+         */
+        public Tokenizer(String source) {
+            this.source = source;
+        }
+
+        /** Returns {@code true} if at least one token is available. */
+        public boolean hasNext() {
+            return foundDot || !isEol();
+        }
+
+        /** Returns next token. */
+        public String nextToken() {
+            if (!hasNext()) {
+                throw new NoSuchElementException("No more tokens available.");
+            } else if (isEol()) {
+                assert foundDot;
+
+                foundDot = false;
+
+                return "";
+            }
+
+            boolean quoted = currentChar() == '"';
+
+            if (quoted) {
+                currentPosition++;
+            }
+
+            int start = currentPosition;
+            StringBuilder sb = new StringBuilder();
+            foundDot = false;
+
+            if (!quoted && !isEol()) {
+                if (identifierStart(source.codePointAt(currentPosition))) {
+                    currentPosition++;
+                } else {
+                    throwMalformedNameException();
+                }
+            }
+
+            for (; !isEol(); currentPosition++) {
+                char c = currentChar();
+
+                if (c == '"') {
+                    if (!quoted) {
+                        throwMalformedNameException();
+                    }
+
+                    if (hasNextChar() && nextChar() == '"') {  // quote is 
escaped
+                        sb.append(source, start, currentPosition + 1);
+
+                        start = currentPosition + 2;
+                        currentPosition += 1;
+
+                        continue;
+                    } else if (!hasNextChar() || nextChar() == '.') {
+                        // looks like we just found a closing quote
+                        sb.append(source, start, currentPosition);
+
+                        foundDot = hasNextChar();
+                        currentPosition += 2;
+
+                        return sb.toString();
+                    }
+
+                    throwMalformedNameException();
+                } else if (c == '.') {
+                    if (quoted) {
+                        continue;
+                    }
+
+                    sb.append(source, start, currentPosition);
+
+                    currentPosition++;
+                    foundDot = true;
+
+                    return sb.toString().toUpperCase();
+                } else if (!quoted
+                        && 
!identifierStart(source.codePointAt(currentPosition))
+                        && 
!identifierExtend(source.codePointAt(currentPosition))
+                ) {
+                    throwMalformedNameException();
+                }
+            }
+
+            if (quoted) {
+                // seems like there is no closing quote
+                throwMalformedNameException();
+            }
+
+            return source.substring(start).toUpperCase();
+        }
+
+        private boolean isEol() {
+            return currentPosition >= source.length();
+        }
+
+        private char currentChar() {
+            return source.charAt(currentPosition);
+        }
+
+        private boolean hasNextChar() {
+            return currentPosition + 1 < source.length();
+        }
+
+        private char nextChar() {
+            return source.charAt(currentPosition + 1);
+        }
+
+        private void throwMalformedNameException() {
+            throw new IllegalArgumentException("Malformed identifier 
[identifier=" + source + ", pos=" + currentPosition + ']');
+        }
+    }
+}
diff --git 
a/modules/api/src/test/java/org/apache/ignite/table/QualifiedNameTest.java 
b/modules/api/src/test/java/org/apache/ignite/table/QualifiedNameTest.java
new file mode 100644
index 0000000000..c0676ab418
--- /dev/null
+++ b/modules/api/src/test/java/org/apache/ignite/table/QualifiedNameTest.java
@@ -0,0 +1,281 @@
+/*
+ * 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;
+
+import static org.apache.ignite.lang.util.IgniteNameUtils.quoteIfNeeded;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.anyOf;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+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;
+
+/**
+ * Test cases to verify {@link QualifiedName}.
+ */
+public class QualifiedNameTest {
+    private static Arguments[] validSimpleNamesArgs() {
+        return new Arguments[]{
+                // Uppercased
+                Arguments.of("foo", "FOO"),
+                Arguments.of("fOo", "FOO"),
+                Arguments.of("FOO", "FOO"),
+                Arguments.of("f23", "F23"),
+                Arguments.of("foo_", "FOO_"),
+                Arguments.of("foo_1", "FOO_1"),
+
+                // Quoted
+                Arguments.of("\"FOO\"", "FOO"),
+                Arguments.of("\"foo\"", "foo"),
+                Arguments.of("\"fOo\"", "fOo"),
+                Arguments.of("\"_foo\"", "_foo"),
+                Arguments.of("\"$foo\"", "$foo"),
+                Arguments.of("\"%foo\"", "%foo"),
+                Arguments.of("\"foo_\"", "foo_"),
+                Arguments.of("\"foo$\"", "foo$"),
+                Arguments.of("\"foo%\"", "foo%"),
+                Arguments.of("\"@#$\"", "@#$"),
+                Arguments.of("\"f.f\"", "f.f"),
+                Arguments.of("\"   \"", "   "),
+
+                // Escaped
+                Arguments.of("\"f\"\"f\"", "f\"f"),
+                Arguments.of("\"f\"\"\"\"f\"", "f\"\"f"),
+                Arguments.of("\"\"\"bar\"\"\"", "\"bar\""),
+                Arguments.of("\"\"\"\"\"bar\"\"\"", "\"\"bar\"")
+        };
+    }
+
+    private static Arguments[] malformedSimpleNamesArgs() {
+        return new Arguments[]{
+                // Empty names
+                Arguments.of(""),
+                Arguments.of(" "),
+
+                // Unexpected delimiters
+                Arguments.of(".f"),
+                Arguments.of("f."),
+                Arguments.of("."),
+
+                // Unquoted names with Invalid characters
+                Arguments.of("f f"),
+                Arguments.of("1o0"),
+                Arguments.of("@#$"),
+                Arguments.of("_foo"),
+                Arguments.of("foo$"),
+                Arguments.of("foo%"),
+                Arguments.of("foo&"),
+
+                // Invalid escape sequences
+                Arguments.of("f\"f"),
+                Arguments.of("f\"\"f"),
+                Arguments.of("\"foo"),
+                Arguments.of("\"fo\"o\"")
+        };
+    }
+
+    private static Arguments[] malformedCanonicalNamesArgs() {
+        return new Arguments[]{
+                Arguments.of("foo."),
+                Arguments.of(".bar"),
+                Arguments.of("."),
+                Arguments.of("foo..bar"),
+                Arguments.of("foo.bar."),
+                Arguments.of("foo.."),
+
+                Arguments.of("@#$.bar"),
+                Arguments.of("foo.@#$"),
+                Arguments.of("@#$"),
+                Arguments.of("1oo.bar"),
+                Arguments.of("foo.1ar"),
+                Arguments.of("1oo"),
+                Arguments.of("_foo.bar"),
+                Arguments.of("foo._bar")
+        };
+    }
+
+    private static Arguments[] validCanonicalNamesArgs() {
+        return new Arguments[]{
+                Arguments.of("\"foo.bar\".baz", "foo.bar", "BAZ"),
+                Arguments.of("foo.\"bar.baz\"", "FOO", "bar.baz"),
+
+                Arguments.of("\"foo.\"\"bar\"\"\".baz", "foo.\"bar\"", "BAZ"),
+                Arguments.of("foo.\"bar.\"\"baz\"", "FOO", "bar.\"baz")
+        };
+    }
+
+    @SuppressWarnings("DataFlowIssue")
+    @Test
+    public void invalidNullNames() {
+        assertThrows(NullPointerException.class, () -> 
QualifiedName.parse(null));
+        assertThrows(NullPointerException.class, () -> 
QualifiedName.fromSimple(null));
+        assertThrows(NullPointerException.class, () -> QualifiedName.of("s1", 
null));
+        assertThrows(NullPointerException.class, () -> QualifiedName.of(null, 
null));
+    }
+
+    @Test
+    public void defaultSchemaName() {
+        assertEquals(QualifiedName.DEFAULT_SCHEMA_NAME, 
QualifiedName.parse("foo").schemaName());
+        assertEquals(QualifiedName.DEFAULT_SCHEMA_NAME, 
QualifiedName.fromSimple("foo").schemaName());
+        assertEquals(QualifiedName.DEFAULT_SCHEMA_NAME, QualifiedName.of(null, 
"foo").schemaName());
+    }
+
+    @Test
+    public void canonicalForm() {
+        assertThat(QualifiedName.parse("foo.bar").toCanonicalForm(), 
equalTo("FOO.BAR"));
+        assertThat(QualifiedName.parse("\"foo\".\"bar\"").toCanonicalForm(), 
equalTo("\"foo\".\"bar\""));
+    }
+
+    @ParameterizedTest
+    @MethodSource("validSimpleNamesArgs")
+    void validSimpleNames(String actual, String expectedIdentifier) {
+        QualifiedName simple = QualifiedName.fromSimple(actual);
+        QualifiedName parsed = QualifiedName.parse(actual);
+        QualifiedName of = QualifiedName.of(null, actual);
+
+        assertThat(simple.objectName(), equalTo(expectedIdentifier));
+        assertThat(parsed.objectName(), equalTo(expectedIdentifier));
+        assertThat(of.objectName(), equalTo(expectedIdentifier));
+
+        assertEquals(parsed, simple);
+        assertEquals(of, simple);
+    }
+
+    @ParameterizedTest
+    @MethodSource("validSimpleNamesArgs")
+    public void validCanonicalNames(String source, String expectedIdentifier) {
+        QualifiedName parsed = QualifiedName.parse(source + '.' + source);
+
+        assertThat(parsed.schemaName(), equalTo(expectedIdentifier));
+        assertThat(parsed.objectName(), equalTo(expectedIdentifier));
+
+        QualifiedName of = QualifiedName.of(source, source);
+
+        assertThat(of.schemaName(), equalTo(expectedIdentifier));
+        assertThat(of.objectName(), equalTo(expectedIdentifier));
+
+        assertEquals(of, parsed);
+
+        // Canonical form should parsed to the equal object.
+        assertEquals(parsed, QualifiedName.parse(parsed.toCanonicalForm()));
+    }
+
+    @ParameterizedTest
+    @MethodSource("validCanonicalNamesArgs")
+    public void validCanonicalNames(String source, String schemaIdentifier, 
String objectIdentifier) {
+        QualifiedName parsed = QualifiedName.parse(source);
+
+        assertThat(parsed.schemaName(), equalTo(schemaIdentifier));
+        assertThat(parsed.objectName(), equalTo(objectIdentifier));
+
+        assertThat(parsed.toCanonicalForm(), 
equalTo(canonicalName(schemaIdentifier, objectIdentifier)));
+
+        // Canonical form should parsed to the equal object.
+        assertEquals(parsed, QualifiedName.parse(parsed.toCanonicalForm()));
+
+        assertEquals(parsed, QualifiedName.of(quoteIfNeeded(schemaIdentifier), 
quoteIfNeeded(objectIdentifier)));
+    }
+
+    @ParameterizedTest
+    @MethodSource("malformedSimpleNamesArgs")
+    public void malformedSimpleNames(String source) {
+        { // fromSimple
+            IllegalArgumentException ex = 
assertThrows(IllegalArgumentException.class, () -> 
QualifiedName.fromSimple(source));
+
+            assertThat(ex.getMessage(), is(anyOf(
+                    equalTo("Object identifier can't be empty."),
+                    equalTo("Fully qualified name is not expected [name=" + 
source + "]"),
+                    containsString("Malformed identifier [identifier=" + 
source))));
+        }
+
+        { // parse
+            IllegalArgumentException ex = 
assertThrows(IllegalArgumentException.class, () -> QualifiedName.parse(source));
+
+            assertThat(ex.getMessage(), is(anyOf(
+                    equalTo("Schema identifier can't be empty."),
+                    equalTo("Object identifier can't be empty."),
+                    containsString("Malformed identifier [identifier=" + 
source))));
+        }
+
+        { // schemaName
+            IllegalArgumentException ex = 
assertThrows(IllegalArgumentException.class, () -> QualifiedName.of(source, 
"bar"));
+
+            assertThat(ex.getMessage(), is(anyOf(
+                    equalTo("Fully qualified name is not expected [name=" + 
source + "]"),
+                    equalTo("Schema identifier can't be empty."),
+                    containsString("Malformed identifier [identifier=" + 
source))
+            ));
+        }
+
+        { // objectName
+            IllegalArgumentException ex = 
assertThrows(IllegalArgumentException.class, () -> QualifiedName.of(null, 
source));
+
+            assertThat(ex.getMessage(), is(anyOf(
+                    equalTo("Fully qualified name is not expected [name=" + 
source + "]"),
+                    equalTo("Object identifier can't be empty."),
+                    containsString("Malformed identifier [identifier=" + 
source))
+            ));
+        }
+    }
+
+    @Test
+    public void unexpectedCanonicalName() {
+        String canonicalName = "f.f";
+
+        { // fromSimple
+            IllegalArgumentException ex = 
assertThrows(IllegalArgumentException.class, () -> 
QualifiedName.fromSimple(canonicalName));
+
+            assertThat(ex.getMessage(), equalTo("Fully qualified name is not 
expected [name=" + canonicalName + "]"));
+        }
+
+        { // schemaName
+            IllegalArgumentException ex = 
assertThrows(IllegalArgumentException.class, () -> 
QualifiedName.of(canonicalName, "bar"));
+
+            assertThat(ex.getMessage(), equalTo("Fully qualified name is not 
expected [name=" + canonicalName + "]"));
+        }
+
+        { // objectName
+            IllegalArgumentException ex = 
assertThrows(IllegalArgumentException.class, () -> QualifiedName.of(null, 
canonicalName));
+
+            assertThat(ex.getMessage(), equalTo("Fully qualified name is not 
expected [name=" + canonicalName + "]"));
+        }
+    }
+
+    @ParameterizedTest
+    @MethodSource("malformedCanonicalNamesArgs")
+    public void malformedCanonicalNames(String source) {
+        IllegalArgumentException ex = 
assertThrows(IllegalArgumentException.class, () -> QualifiedName.parse(source));
+
+        assertThat(ex.getMessage(), is(anyOf(
+                equalTo("Object identifier can't be empty."),
+                equalTo("Canonical name format mismatch: " + source),
+                containsString("Malformed identifier [identifier=" + source)
+        )));
+    }
+
+    // TODO https://issues.apache.org/jira/browse/IGNITE-24021 Move to 
IgniteNameUtils
+    private static String canonicalName(String schemaName, String objectName) {
+        return schemaName == null ? quoteIfNeeded(objectName) : 
quoteIfNeeded(schemaName) + '.' + quoteIfNeeded(objectName);
+    }
+}
diff --git 
a/modules/core/src/main/java/org/apache/ignite/internal/sql/SqlCommon.java 
b/modules/core/src/main/java/org/apache/ignite/internal/sql/SqlCommon.java
index 711f7968fa..f04db58a5f 100644
--- a/modules/core/src/main/java/org/apache/ignite/internal/sql/SqlCommon.java
+++ b/modules/core/src/main/java/org/apache/ignite/internal/sql/SqlCommon.java
@@ -17,12 +17,15 @@
 
 package org.apache.ignite.internal.sql;
 
+import org.apache.ignite.table.QualifiedName;
+
 /**
  * Common SQL utilities.
  */
 public final class SqlCommon {
+    // TODO https://issues.apache.org/jira/browse/IGNITE-24021: remove this.
     /** Name of the default schema. */
-    public static final String DEFAULT_SCHEMA_NAME = "PUBLIC";
+    public static final String DEFAULT_SCHEMA_NAME = 
QualifiedName.DEFAULT_SCHEMA_NAME;
 
     /** Default page size. */
     public static final int DEFAULT_PAGE_SIZE = 1024;

Reply via email to