Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2030,6 +2030,7 @@ nonReserved
| CAST
| CATALOG
| CATALOGS
| CEL
| CHAIN
| CIPHER
| CHAR
Expand Down Expand Up @@ -2187,6 +2188,7 @@ nonReserved
| LOGICAL
| MANUAL
| MAP
| MAPPING
| MATCHED
| MATCH_ALL
| MATCH_ANY
Expand Down Expand Up @@ -2294,6 +2296,7 @@ nonReserved
| ROOT
| ROTATE
| ROUTINE
| RULE
| S3
| SAMPLE
| SAN
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,38 @@
package org.apache.doris.nereids.parser;

import org.apache.doris.nereids.exceptions.ParseException;
import org.apache.doris.nereids.trees.plans.Plan;
import org.apache.doris.nereids.trees.plans.commands.CreateRoleMappingCommand;
import org.apache.doris.nereids.trees.plans.commands.DropRoleMappingCommand;
import org.apache.doris.nereids.trees.plans.logical.LogicalPlan;
import org.apache.doris.nereids.trees.plans.logical.LogicalProject;
import org.apache.doris.qe.ConnectContext;

import com.google.common.collect.ImmutableSet;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.List;

public class RoleMappingParserTest {

private final NereidsParser parser = new NereidsParser();

@BeforeEach
public void setUp() {
// parsing some statements (qualified column refs, INSERT target) reads session state
ConnectContext ctx = new ConnectContext();
ctx.setDatabase("test");
ctx.setThreadLocalInfo();
}

@AfterEach
public void tearDown() {
ConnectContext.remove();
}

@Test
public void testCreateRoleMappingParse() {
LogicalPlan plan = parser.parseSingle("CREATE ROLE MAPPING IF NOT EXISTS corp_mapping "
Expand Down Expand Up @@ -74,4 +94,51 @@ public void testDropRoleMappingParse() {
DropRoleMappingCommand drop2 = (DropRoleMappingCommand) plan2;
Assertions.assertTrue(drop2.isIfExists());
}

/**
* RULE/CEL/MAPPING are keywords introduced by the role-mapping DDL. They are non-reserved,
* so they must still be usable as ordinary identifiers (column names, etc.). Otherwise legacy
* SQL such as `INSERT INTO t(..., RULE, ...)` breaks with "mismatched input 'RULE'".
*/
@Test
public void testRoleMappingKeywordsAsIdentifier() {
LogicalPlan plan = parser.parseSingle("SELECT rule, cel, mapping FROM t");
// the top plan is an UnboundResultSink wrapping the project; descend to the project
Plan node = plan;
while (!(node instanceof LogicalProject) && !node.children().isEmpty()) {
node = node.child(0);
}
Assertions.assertInstanceOf(LogicalProject.class, node);
List<String> names = ((LogicalProject<?>) node).getProjects().stream()
.map(p -> p.getName()).collect(java.util.stream.Collectors.toList());
Assertions.assertEquals(3, names.size());
Assertions.assertTrue(names.get(0).equalsIgnoreCase("rule"), names.toString());
Assertions.assertTrue(names.get(1).equalsIgnoreCase("cel"), names.toString());
Assertions.assertTrue(names.get(2).equalsIgnoreCase("mapping"), names.toString());

// the keywords must also work as a table name / alias
Assertions.assertDoesNotThrow(() ->
parser.parseSingle("SELECT rule.cel FROM mapping AS rule"));

// the original failing case: keywords appearing in an INSERT column list
Assertions.assertDoesNotThrow(() -> parser.parseSingle(
"INSERT INTO fnd_rnk_info(query_level, rule, mapping) "
+ "SELECT query_level, rule, mapping FROM t"));
}

/**
* Making RULE/MAPPING non-reserved must not regress the role-mapping DDL: the parser still
* has to route `CREATE/DROP ROLE MAPPING ...` to the role-mapping command rather than treating
* MAPPING as a role name.
*/
@Test
public void testRoleMappingDdlNotAmbiguous() {
Assertions.assertInstanceOf(CreateRoleMappingCommand.class, parser.parseSingle(
"CREATE ROLE MAPPING corp_mapping ON AUTHENTICATION INTEGRATION corp_oidc "
+ "RULE ( USING CEL 'true' GRANT ROLE analyst )"));
Assertions.assertInstanceOf(DropRoleMappingCommand.class,
parser.parseSingle("DROP ROLE MAPPING corp_mapping"));
// bare `CREATE ROLE mapping` now creates a role literally named "mapping"
Assertions.assertFalse(parser.parseSingle("CREATE ROLE mapping") instanceof CreateRoleMappingCommand);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// 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.

// RULE / CEL / MAPPING are keywords introduced by the role-mapping DDL. They are non-reserved,
// so they must keep working as ordinary column / table identifiers. This guards against the
// regression where `INSERT INTO t(..., RULE, ...)` failed with "mismatched input 'RULE'".
suite("test_role_mapping_keyword", "query,p0") {
def tbl = "test_role_mapping_keyword_tbl"
sql "drop table if exists ${tbl}"
sql """
create table ${tbl}(
`query_level` int NOT NULL,
`rule` varchar(64),
`mapping` varchar(64),
`cel` varchar(64)
) ENGINE=OLAP
DUPLICATE KEY(`query_level`)
DISTRIBUTED BY HASH(`query_level`) BUCKETS 1
PROPERTIES ("replication_allocation" = "tag.location.default: 1");
"""

// keywords in an INSERT column list -- the original failing shape
sql """insert into ${tbl}(query_level, rule, mapping, cel) values (1, 'r1', 'm1', 'c1')"""

// Assert inline instead of using qt_/order_qt_ golden files: this test only needs to prove the
// keywords parse as identifiers and round-trip correctly, so explicit asserts keep it self-contained
// (no .out file to generate/commit).

// keywords as projection items
def r1 = sql "select rule, mapping, cel from ${tbl} order by query_level"
assertEquals(1, r1.size())
assertEquals("r1", r1[0][0])
assertEquals("m1", r1[0][1])
assertEquals("c1", r1[0][2])

// keyword as table alias and qualified column reference
def r2 = sql "select rule.rule from ${tbl} as rule"
assertEquals(1, r2.size())
assertEquals("r1", r2[0][0])

// keywords in a WHERE predicate
def r3 = sql "select query_level from ${tbl} where rule = 'r1' and cel = 'c1' order by query_level"
assertEquals(1, r3.size())
assertEquals(1, r3[0][0])

// insert ... select carrying the keywords through both the target and source column lists
sql """insert into ${tbl}(query_level, rule, mapping, cel)
select 2, rule, mapping, cel from ${tbl}"""
def r4 = sql "select query_level, rule, mapping, cel from ${tbl} order by query_level"
assertEquals(2, r4.size())
assertEquals(1, r4[0][0])
assertEquals("r1", r4[0][1])
assertEquals("m1", r4[0][2])
assertEquals("c1", r4[0][3])
assertEquals(2, r4[1][0])
assertEquals("r1", r4[1][1])
assertEquals("m1", r4[1][2])
assertEquals("c1", r4[1][3])

sql "drop table if exists ${tbl}"
}
Loading