diff --git a/docs/reference/sql/language/syntax/lexic/index.asciidoc b/docs/reference/sql/language/syntax/lexic/index.asciidoc index a668ee724e5..9b2f78c35cd 100644 --- a/docs/reference/sql/language/syntax/lexic/index.asciidoc +++ b/docs/reference/sql/language/syntax/lexic/index.asciidoc @@ -121,6 +121,9 @@ SELECT "first_name" <1> <1> Double quotes `"` used for column and table identifiers <2> Single quotes `'` used for a string literal +NOTE:: to escape single or double quotes, one needs to use that specific quote one more time. For example, the literal `John's` can be escaped like +`SELECT 'John''s' AS name`. The same goes for double quotes escaping - `SELECT 123 AS "test""number"` will display as a result a column with the name `test"number`. + [[sql-syntax-special-chars]] ==== Special characters diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/parser/IdentifierBuilder.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/parser/IdentifierBuilder.java index 37adb44a955..e391850dd17 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/parser/IdentifierBuilder.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/parser/IdentifierBuilder.java @@ -25,12 +25,12 @@ abstract class IdentifierBuilder extends AbstractBuilder { ParseTree tree = ctx.name != null ? ctx.name : ctx.TABLE_IDENTIFIER(); String index = tree.getText(); - return new TableIdentifier(source, visitIdentifier(ctx.catalog), index); + return new TableIdentifier(source, visitIdentifier(ctx.catalog), unquoteIdentifier(index)); } @Override public String visitIdentifier(IdentifierContext ctx) { - return ctx == null ? null : ctx.getText(); + return ctx == null ? null : unquoteIdentifier(ctx.getText()); } @Override @@ -41,4 +41,8 @@ abstract class IdentifierBuilder extends AbstractBuilder { return Strings.collectionToDelimitedString(visitList(ctx.identifier(), String.class), "."); } + + private static String unquoteIdentifier(String identifier) { + return identifier.replace("\"\"", "\""); + } } diff --git a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/parser/SqlParserTests.java b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/parser/SqlParserTests.java index f9b0fc18bca..ca31e32b2ed 100644 --- a/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/parser/SqlParserTests.java +++ b/x-pack/plugin/sql/src/test/java/org/elasticsearch/xpack/sql/parser/SqlParserTests.java @@ -6,7 +6,10 @@ package org.elasticsearch.xpack.sql.parser; import com.google.common.base.Joiner; + import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.sql.expression.Alias; +import org.elasticsearch.xpack.sql.expression.Literal; import org.elasticsearch.xpack.sql.expression.NamedExpression; import org.elasticsearch.xpack.sql.expression.Order; import org.elasticsearch.xpack.sql.expression.UnresolvedAttribute; @@ -21,6 +24,7 @@ import org.elasticsearch.xpack.sql.plan.logical.Filter; import org.elasticsearch.xpack.sql.plan.logical.LogicalPlan; import org.elasticsearch.xpack.sql.plan.logical.OrderBy; import org.elasticsearch.xpack.sql.plan.logical.Project; +import org.elasticsearch.xpack.sql.plan.logical.UnresolvedRelation; import java.util.ArrayList; import java.util.List; @@ -46,6 +50,24 @@ public class SqlParserTests extends ESTestCase { return type.cast(p); } + public void testEscapeDoubleQuotes() { + Project project = project(parseStatement("SELECT bar FROM \"fo\"\"o\"")); + assertTrue(project.child() instanceof UnresolvedRelation); + assertEquals("fo\"o", ((UnresolvedRelation) project.child()).table().index()); + } + + public void testEscapeSingleQuotes() { + Alias a = singleProjection(project(parseStatement("SELECT '''ab''c' AS \"escaped_text\"")), Alias.class); + assertEquals("'ab'c", ((Literal) a.child()).value()); + assertEquals("escaped_text", a.name()); + } + + public void testEscapeSingleAndDoubleQuotes() { + Alias a = singleProjection(project(parseStatement("SELECT 'ab''c' AS \"escaped\"\"text\"")), Alias.class); + assertEquals("ab'c", ((Literal) a.child()).value()); + assertEquals("escaped\"text", a.name()); + } + public void testSelectField() { UnresolvedAttribute a = singleProjection(project(parseStatement("SELECT bar FROM foo")), UnresolvedAttribute.class); assertEquals("bar", a.name());