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" ->
"TBL0", "\"Tbl0\"" -> "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" -> "\"myColumn\"", "MYCOLUMN" ->
+ * "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;