diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilderForSyncMv.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilderForSyncMv.java index 1a3e19c8e0d0c6..c53f1392d45ef5 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilderForSyncMv.java +++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/LogicalPlanBuilderForSyncMv.java @@ -33,6 +33,7 @@ import org.apache.doris.nereids.trees.expressions.visitor.DefaultExpressionRewriter; import org.apache.doris.nereids.trees.plans.Plan; import org.apache.doris.nereids.trees.plans.commands.CreateMTMVCommand; +import org.apache.doris.nereids.trees.plans.commands.info.BaseViewInfo; import org.apache.doris.nereids.trees.plans.logical.LogicalAggregate; import org.apache.doris.nereids.trees.plans.logical.LogicalPlan; import org.apache.doris.nereids.util.PlanUtils; @@ -109,7 +110,7 @@ public LogicalPlan visitQuery(DorisParser.QueryContext ctx) { for (Expression expr : outputs) { aggStateFunctionFinder.find(expr, indexInSqlToString); } - querySql = Optional.of(rewriteSql(originSql, indexInSqlToString)); + querySql = Optional.of(BaseViewInfo.rewriteSql(indexInSqlToString, originSql)); } else { querySql = Optional.of(originSql); } @@ -193,18 +194,4 @@ public Expression visitUnboundFunction(UnboundFunction unboundFunction, return unboundFunction; } } - - private static String rewriteSql(String querySql, - Map, String> indexStringSqlMap) { - StringBuilder builder = new StringBuilder(); - int beg = 0; - for (Map.Entry, String> entry : indexStringSqlMap.entrySet()) { - Pair index = entry.getKey(); - builder.append(querySql, beg, index.first); - builder.append(entry.getValue()); - beg = index.second + 1; - } - builder.append(querySql, beg, querySql.length()); - return builder.toString(); - } } diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/NereidsParser.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/NereidsParser.java index 7a30b276da158f..0484c0145dc8d4 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/NereidsParser.java +++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/parser/NereidsParser.java @@ -200,7 +200,10 @@ public static Optional> tryParseExplainPlan(String return Optional.empty(); } ExplainOptions explainOptions = new ExplainOptions(explainLevel, showPlanProcess); - return Optional.of(Pair.of(explainOptions, sql.substring(explainPlanBody.getStartIndex()))); + // The parser counts a supplementary character such as an emoji as one character, while sql + // counts it as two. + return Optional.of(Pair.of(explainOptions, + sql.substring(sql.offsetByCodePoints(0, explainPlanBody.getStartIndex())))); } catch (Throwable t) { return Optional.empty(); } @@ -349,7 +352,7 @@ private T parse(String sql, @Nullable LogicalPlanBuilder logicalPlanBuilder, CommonTokenStream tokenStream = parseLeanTokens(sql); ParserRuleContext tree = toAst(tokenStream, parseFunction); LogicalPlanBuilder realLogicalPlanBuilder = logicalPlanBuilder == null - ? new LogicalPlanBuilder(getHintMap(sql, tokenStream, DorisParser::selectHint)) + ? new LogicalPlanBuilder(getHintMap(tokenStream, DorisParser::selectHint)) : logicalPlanBuilder; return (T) realLogicalPlanBuilder.visit(tree); } @@ -358,7 +361,7 @@ public LogicalPlan parseForCreateView(String sql) { CommonTokenStream tokenStream = parseLeanTokens(sql); ParserRuleContext tree = toAst(tokenStream, DorisParser::singleStatement); LogicalPlanBuilder realLogicalPlanBuilder = new LogicalPlanBuilderForCreateView( - getHintMap(sql, tokenStream, DorisParser::selectHint)); + getHintMap(tokenStream, DorisParser::selectHint)); return (LogicalPlan) realLogicalPlanBuilder.visit(tree); } @@ -378,13 +381,13 @@ public Optional parseForSyncMv(String sql) { CommonTokenStream tokenStream = parseLeanTokens(sql); ParserRuleContext tree = toAst(tokenStream, DorisParser::singleStatement); LogicalPlanBuilderForSyncMv logicalPlanBuilderForSyncMv = new LogicalPlanBuilderForSyncMv( - getHintMap(sql, tokenStream, DorisParser::selectHint)); + getHintMap(tokenStream, DorisParser::selectHint)); logicalPlanBuilderForSyncMv.visit(tree); return logicalPlanBuilderForSyncMv.getQuerySql(); } /** get hint map */ - public static Map getHintMap(String sql, CommonTokenStream hintTokenStream, + public static Map getHintMap(CommonTokenStream hintTokenStream, Function parseFunction) { // parse hint first round Map selectHintMap = Maps.newHashMap(); @@ -392,8 +395,10 @@ public static Map getHintMap(String sql, CommonToken Iterator tokenIterator = hintTokenStream.getTokens().iterator(); Token hintToken = tokenIterator.hasNext() ? tokenIterator.next() : null; while (hintToken != null && hintToken.getType() != DorisLexer.EOF) { - if (hintToken.getChannel() == 2 && sql.charAt(hintToken.getStartIndex() + 2) == '+') { - String hintSql = sql.substring(hintToken.getStartIndex() + 3, hintToken.getStopIndex() + 1); + // Token positions count a supplementary character such as an emoji as one character, so read + // the hint from the token instead of cutting the SQL string. + if (hintToken.getChannel() == 2 && hintToken.getText().startsWith("/*+")) { + String hintSql = hintToken.getText().substring(3); DorisLexer newHintLexer = new DorisLexer(CaseInsensitiveStream.fromString(hintSql)); CommonTokenStream newHintTokenStream = new CommonTokenStream(newHintLexer); DorisParser hintParser = new DorisParser(newHintTokenStream); diff --git a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/BaseViewInfo.java b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/BaseViewInfo.java index 54b1969db56c5b..892c67a4e4866c 100644 --- a/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/BaseViewInfo.java +++ b/fe/fe-core/src/main/java/org/apache/doris/nereids/trees/plans/commands/info/BaseViewInfo.java @@ -81,9 +81,9 @@ import com.google.common.collect.Lists; import com.google.common.collect.Sets; import org.antlr.v4.runtime.ParserRuleContext; +import org.antlr.v4.runtime.misc.Interval; import org.antlr.v4.runtime.tree.ParseTree; import org.antlr.v4.runtime.tree.RuleNode; -import org.apache.commons.lang3.StringUtils; import java.util.List; import java.util.Map; @@ -128,18 +128,25 @@ protected void analyzeAndFillRewriteSqlMap(String sql, ConnectContext ctx) throw * Add the full path to the field * * @param indexStringSqlMap key is the start and end position of the sql substring that needs to be replaced, - * and value is the new string used for replacement. + * and value is the new string used for replacement. The positions are the token positions the + * parser reports for querySql. * @param querySql origin query sql * @return sql rewritten sql */ public static String rewriteSql(TreeMap, String> indexStringSqlMap, String querySql) { StringBuilder builder = new StringBuilder(); + // The parser counts a supplementary character such as an emoji as one character, while querySql + // counts it as two. beg and begTokenPosition are the same place on both scales; the keys ascend, + // so each position is converted by scanning forward from the previous one. int beg = 0; + int begTokenPosition = 0; for (Map.Entry, String> entry : indexStringSqlMap.entrySet()) { Pair index = entry.getKey(); - builder.append(querySql, beg, index.first); + int start = querySql.offsetByCodePoints(beg, index.first - begTokenPosition); + builder.append(querySql, beg, start); builder.append(entry.getValue()); - beg = index.second + 1; + beg = querySql.offsetByCodePoints(start, index.second + 1 - index.first); + begTokenPosition = index.second + 1; } builder.append(querySql, beg, querySql.length()); return builder.toString(); @@ -167,10 +174,11 @@ public static String rewriteProjectsToUserDefineAlias(String resSql, List namedExpressionContexts = finder.getNamedExpressionContexts(); StringBuilder replaceWithColsBuilder = new StringBuilder(); for (int i = 0; i < namedExpressionContexts.size(); ++i) { - NamedExpressionContext namedExpressionContext = namedExpressionContexts.get(i); - int start = namedExpressionContext.expression().start.getStartIndex(); - int stop = namedExpressionContext.expression().stop.getStopIndex(); - replaceWithColsBuilder.append(resSql, start, stop + 1); + ParserRuleContext expression = namedExpressionContexts.get(i).expression(); + // Token positions count a supplementary character such as an emoji as one character, so read + // the expression from the parser's input instead of cutting resSql. + replaceWithColsBuilder.append(expression.start.getInputStream().getText( + Interval.of(expression.start.getStartIndex(), expression.stop.getStopIndex()))); replaceWithColsBuilder.append(" AS `"); String escapeBacktick = finalColNames.get(i).replace("`", "``"); replaceWithColsBuilder.append(escapeBacktick); @@ -179,9 +187,9 @@ public static String rewriteProjectsToUserDefineAlias(String resSql, List, String> selectColumns = new TreeMap<>(new Pair.PairComparator<>()); + selectColumns.put(finder.getIndex(), replaceWithColsBuilder.toString()); + return rewriteSql(selectColumns, resSql); } protected void createFinalCols(List outputs) throws org.apache.doris.common.AnalysisException { diff --git a/fe/fe-core/src/test/java/org/apache/doris/catalog/CreateViewTest.java b/fe/fe-core/src/test/java/org/apache/doris/catalog/CreateViewTest.java index bec3e35a6266cb..b3ea280fb178fe 100644 --- a/fe/fe-core/src/test/java/org/apache/doris/catalog/CreateViewTest.java +++ b/fe/fe-core/src/test/java/org/apache/doris/catalog/CreateViewTest.java @@ -268,6 +268,26 @@ public void testCreateViewWithDefinedColumnsRewritesAliases() throws Exception { view.getInlineViewDef()); } + @Test + public void testCreateViewRewritesAfterSupplementaryCharacters() throws Exception { + // The parser counts a supplementary character such as an emoji as one character, while + // the Java string of the view definition counts it as two. + ExceptionChecker.expectThrowsNoException(() -> createView( + "create view test.supplementary_char_view as select 'πŸ˜€πŸ˜€' as e, k1 from test.tbl1;")); + ExceptionChecker.expectThrowsNoException(() -> createView( + "create view test.supplementary_char_alias_view(c1, c2) as select 'πŸ˜€πŸ˜€', k1 from test.tbl1;")); + + Database db = Env.getCurrentInternalCatalog().getDbOrDdlException("test"); + View view = (View) db.getTableOrDdlException("supplementary_char_view"); + Assertions.assertEquals( + "select 'πŸ˜€πŸ˜€' as `e`, `internal`.`test`.`tbl1`.`k1` from `internal`.`test`.`tbl1`", + view.getInlineViewDef()); + View aliasView = (View) db.getTableOrDdlException("supplementary_char_alias_view"); + Assertions.assertEquals( + "select 'πŸ˜€πŸ˜€' AS `c1`, `internal`.`test`.`tbl1`.`k1` AS `c2` from `internal`.`test`.`tbl1`", + aliasView.getInlineViewDef()); + } + @Test public void testViewPreservesVarbinary() throws Exception { createView("create view test.vb_view as select X'AB' as vb;"); diff --git a/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/EncryptSQLTest.java b/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/EncryptSQLTest.java index 5ccb3ca9400524..dab80292149f26 100644 --- a/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/EncryptSQLTest.java +++ b/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/EncryptSQLTest.java @@ -505,6 +505,98 @@ public void testAlterUserPasswordMasking() throws Exception { } } + @Test + public void testEncryptionAfterSupplementaryCharacters() throws Exception { + ctx.setDatabase("test"); + try (MockedConstruction ignored = Mockito.mockConstruction(StmtExecutor.class, + Mockito.withSettings().defaultAnswer(Mockito.CALLS_REAL_METHODS), + (mock, context) -> { + Mockito.doReturn(false).when(mock).isForwardToMaster(); + Profile profile = new Profile(false, 0, 0); + Deencapsulation.setField(mock, "profile", profile); + Mockito.doReturn(profile).when(mock).getProfile(); + Mockito.doReturn(ctx).when(mock).getContext(); + Mockito.doNothing().when(mock).execute(); + Deencapsulation.setField(mock, "context", ctx); + if (context.arguments().size() >= 2 + && context.arguments().get(1) instanceof StatementBase) { + Deencapsulation.setField(mock, "parsedStmt", + context.arguments().get(1)); + } + if (ctx.getStatementContext() == null) { + ctx.setStatementContext(new StatementContext()); + } + })) { + ctx.setEnv(env); + ctx.setCurrentUserIdentity(UserIdentity.ROOT); + // The parser counts a supplementary character such as an emoji as one character, while + // the Java string of the SQL counts it as two. + String sql = "CREATE CATALOG ctl " + + "PROPERTIES(" + + " \"type\" = \"iceberg\"," + + " \"iceberg.catalog.type\" = \"hadoop\"," + + " \"warehouse\" = \"s3://bucket/dir/key\"," + + " \"s3.access_key\" = \"abc\"," + + " \"s3.secret_key\" = \"πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€secret\"" + + ")"; + String res = "CREATE CATALOG ctl " + + "PROPERTIES(" + + " \"type\" = \"iceberg\"," + + " \"iceberg.catalog.type\" = \"hadoop\"," + + " \"warehouse\" = \"s3://bucket/dir/key\"," + + " \"s3.access_key\" = \"*XXX\"," + + " \"s3.secret_key\" = \"*XXX\"" + + ")"; + parseAndCheck(sql, res); + + sql = "CREATE CATALOG ctl COMMENT \"πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€\" " + + "PROPERTIES(" + + " \"type\" = \"iceberg\"," + + " \"iceberg.catalog.type\" = \"hadoop\"," + + " \"warehouse\" = \"s3://bucket/πŸ˜€/key\"," + + " \"s3.access_key\" = \"abc\"," + + " \"s3.secret_key\" = \"abc\"" + + ")"; + res = "CREATE CATALOG ctl COMMENT \"πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€\" " + + "PROPERTIES(" + + " \"type\" = \"iceberg\"," + + " \"iceberg.catalog.type\" = \"hadoop\"," + + " \"warehouse\" = \"s3://bucket/πŸ˜€/key\"," + + " \"s3.access_key\" = \"*XXX\"," + + " \"s3.secret_key\" = \"*XXX\"" + + ")"; + parseAndCheck(sql, res); + + sql = "CREATE EXTERNAL TABLE broker_tbl(" + + " k1 tinyint) " + + "ENGINE=broker " + + "PROPERTIES(" + + " \"broker_name\" = \"hdfs\"," + + " \"path\" = \"hdfs://abc/πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€/a.txt\"" + + ") " + + "BROKER PROPERTIES(" + + " \"username\" = \"root\"," + + " \"password\" = \"123\"" + + ")"; + res = "CREATE EXTERNAL TABLE broker_tbl(" + + " k1 tinyint) " + + "ENGINE=broker " + + "PROPERTIES(" + + " \"broker_name\" = \"hdfs\"," + + " \"path\" = \"hdfs://abc/πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€/a.txt\"" + + ") " + + "BROKER PROPERTIES(" + + " \"username\" = \"root\"," + + " \"password\" = \"*XXX\"" + + ")"; + parseAndCheck(sql, res); + + sql = "SET PASSWORD FOR 'πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€' = PASSWORD('123456')"; + res = "SET PASSWORD FOR 'πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€πŸ˜€' = PASSWORD('*XXX')"; + parseAndCheck(sql, res); + } + } + private void parseAndCheck(String sql, String expected) throws Exception { processor.executeQuery(sql); AuditEvent event = auditEvents.get(auditEvents.size() - 1); diff --git a/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/SupplementaryCharacterOffsetTest.java b/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/SupplementaryCharacterOffsetTest.java new file mode 100644 index 00000000000000..b55a7e8a8cf119 --- /dev/null +++ b/fe/fe-core/src/test/java/org/apache/doris/nereids/parser/SupplementaryCharacterOffsetTest.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.doris.nereids.parser; + +import org.apache.doris.nereids.properties.SelectHintOrdered; +import org.apache.doris.nereids.trees.plans.Plan; +import org.apache.doris.nereids.trees.plans.logical.LogicalPlan; +import org.apache.doris.nereids.trees.plans.logical.LogicalSelectHint; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.List; + +/** + * The parser counts a supplementary character such as an emoji as one character, while the Java + * string of the SQL counts it as two. These cases cut the SQL text after such characters. + */ +class SupplementaryCharacterOffsetTest extends ParserTestBase { + private final NereidsParser parser = new NereidsParser(); + + @Test + void recognizesHintAfterSupplementaryCharacter() { + LogicalPlan plan = parser.parseSingle("/* πŸ˜€ */ SELECT /*+ ORDERED */ a FROM t"); + List> hintPlans = plan.collectToList(LogicalSelectHint.class::isInstance); + + Assertions.assertEquals(1, hintPlans.size()); + Assertions.assertTrue(hintPlans.get(0).getHints().stream() + .anyMatch(SelectHintOrdered.class::isInstance)); + } + + @Test + void extractsExplainPlanBodyAfterSupplementaryCharacters() { + Assertions.assertEquals("SELECT 100", + NereidsParser.tryParseExplainPlan("EXPLAIN /* abc */ PLAN SELECT 100").orElseThrow().second); + Assertions.assertEquals("SELECT 100", + NereidsParser.tryParseExplainPlan("EXPLAIN /* πŸ˜€πŸ˜€πŸ˜€ */ PLAN SELECT 100").orElseThrow().second); + } + + @Test + void rewritesSyncMvAggregateAfterSupplementaryCharacter() { + Assertions.assertEquals("SELECT /* a */ k1, avg_union(avg_state(k2)) FROM t GROUP BY k1", + parser.parseForSyncMv("CREATE MATERIALIZED VIEW mv AS " + + "SELECT /* a */ k1, avg(k2) FROM t GROUP BY k1").orElseThrow()); + Assertions.assertEquals("SELECT /* πŸ˜€ */ k1, avg_union(avg_state(k2)) FROM t GROUP BY k1", + parser.parseForSyncMv("CREATE MATERIALIZED VIEW mv AS " + + "SELECT /* πŸ˜€ */ k1, avg(k2) FROM t GROUP BY k1").orElseThrow()); + } +} diff --git a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/CreateMTMVCommandTest.java b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/CreateMTMVCommandTest.java index f0fe9fc3da2e82..df810ff920c135 100644 --- a/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/CreateMTMVCommandTest.java +++ b/fe/fe-core/src/test/java/org/apache/doris/nereids/trees/plans/CreateMTMVCommandTest.java @@ -626,6 +626,25 @@ public void testCreateMTMVRewriteQuerySqlWithDefinedColumnsForScanPlan() throws Assertions.assertTrue(createMTMVInfo.getQuerySql().contains("AS `mv_score`")); } + @Test + public void testCreateMTMVRewritesQuerySqlAfterSupplementaryCharacters() throws Exception { + createTable("create table test.mtmv_supplementary_char_base (id int, score int)\n" + + "duplicate key(id)\n" + + "distributed by hash(id) buckets 1\n" + + "properties('replication_num' = '1');"); + + // The parser counts a supplementary character such as an emoji as one character, while + // the Java string of the query counts it as two. + CreateMTMVInfo createMTMVInfo = getPartitionTableInfo("CREATE MATERIALIZED VIEW mtmv_supplementary_char\n" + + " BUILD DEFERRED REFRESH COMPLETE ON MANUAL\n" + + " PROPERTIES ('replication_num' = '1')\n" + + " AS\n" + + " SELECT 'πŸ˜€πŸ˜€' AS e, id FROM mtmv_supplementary_char_base;"); + + Assertions.assertEquals("SELECT 'πŸ˜€πŸ˜€' AS `e`, `internal`.`test`.`mtmv_supplementary_char_base`.`id` " + + "FROM `internal`.`test`.`mtmv_supplementary_char_base`", createMTMVInfo.getQuerySql()); + } + @Test public void testCreateMTMVRewriteQuerySqlWithDefinedColumnsForProjectScanPlan() throws Exception { createTable("create table test.mtmv_project_scan_base (id int, score int)\n" diff --git a/regression-test/data/ddl_p0/create_view_nereids/test_create_view_supplementary_chars.out b/regression-test/data/ddl_p0/create_view_nereids/test_create_view_supplementary_chars.out new file mode 100644 index 00000000000000..05a50df32b21c3 --- /dev/null +++ b/regression-test/data/ddl_p0/create_view_nereids/test_create_view_supplementary_chars.out @@ -0,0 +1,16 @@ +-- This file is automatically generated. You should know what you did if you want to edit this +-- !show_view -- +test_create_view_supplementary_chars_view CREATE VIEW `test_create_view_supplementary_chars_view`\n(`e`,`k1`,`v1`)\n AS select 'πŸ˜€πŸ˜€' as `e`, `internal`.`regression_test_ddl_p0_create_view_nereids`.`test_create_view_supplementary_chars_base`.`k1`, `internal`.`regression_test_ddl_p0_create_view_nereids`.`test_create_view_supplementary_chars_base`.`v1` from `internal`.`regression_test_ddl_p0_create_view_nereids`.`test_create_view_supplementary_chars_base` where `internal`.`regression_test_ddl_p0_create_view_nereids`.`test_create_view_supplementary_chars_base`.`v1` != 'πŸ˜€πŸ˜€πŸ˜€'; utf8mb4 utf8mb4_0900_bin + +-- !select_view -- +πŸ˜€πŸ˜€ 1 aπŸ˜€ +πŸ˜€πŸ˜€ 2 b + +-- !show_alias -- +test_create_view_supplementary_chars_alias CREATE VIEW `test_create_view_supplementary_chars_alias`\n(`c1`,`c2`)\n AS select 'πŸ˜€πŸ˜€' AS `c1`, `internal`.`regression_test_ddl_p0_create_view_nereids`.`test_create_view_supplementary_chars_base`.`k1` AS `c2` from `internal`.`regression_test_ddl_p0_create_view_nereids`.`test_create_view_supplementary_chars_base`; utf8mb4 utf8mb4_0900_bin + +-- !select_alias -- +πŸ˜€πŸ˜€ 1 +πŸ˜€πŸ˜€ 2 +πŸ˜€πŸ˜€ 3 + diff --git a/regression-test/suites/ddl_p0/create_view_nereids/test_create_view_supplementary_chars.groovy b/regression-test/suites/ddl_p0/create_view_nereids/test_create_view_supplementary_chars.groovy new file mode 100644 index 00000000000000..ea0e7636baa034 --- /dev/null +++ b/regression-test/suites/ddl_p0/create_view_nereids/test_create_view_supplementary_chars.groovy @@ -0,0 +1,45 @@ +// 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. + +// The parser counts a supplementary character such as an emoji as one character, while the Java string +// of the view query counts it as two; the view definition is rewritten on that string. +suite("test_create_view_supplementary_chars") { + sql "drop table if exists test_create_view_supplementary_chars_base" + sql """ + create table test_create_view_supplementary_chars_base (k1 int, v1 varchar(32)) + duplicate key(k1) + distributed by hash(k1) buckets 1 + properties('replication_num' = '1') + """ + sql "insert into test_create_view_supplementary_chars_base values (1, 'aπŸ˜€'), (2, 'b'), (3, 'πŸ˜€πŸ˜€πŸ˜€')" + + sql "drop view if exists test_create_view_supplementary_chars_view" + sql """ + create view test_create_view_supplementary_chars_view as + select 'πŸ˜€πŸ˜€' as e, k1, v1 from test_create_view_supplementary_chars_base where v1 != 'πŸ˜€πŸ˜€πŸ˜€' + """ + qt_show_view "show create view test_create_view_supplementary_chars_view" + order_qt_select_view "select * from test_create_view_supplementary_chars_view" + + sql "drop view if exists test_create_view_supplementary_chars_alias" + sql """ + create view test_create_view_supplementary_chars_alias (c1, c2) as + select 'πŸ˜€πŸ˜€', k1 from test_create_view_supplementary_chars_base + """ + qt_show_alias "show create view test_create_view_supplementary_chars_alias" + order_qt_select_alias "select * from test_create_view_supplementary_chars_alias" +}