diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 487a3a7b7861a..c96422fc79193 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -77,6 +77,7 @@ 1. Encrypt: Support SqlServer update statement for Specifying a table alias as the target object when use encrypt feature - [#38733](https://github.com/apache/shardingsphere/pull/38733) 1. Encrypt: Support SqlServer update statement for Specifying a view as the target object when use encrypt feature - [#38896](https://github.com/apache/shardingsphere/pull/38896) 1. Encrypt: Support SqlServer for Using the UPDATE statement with information from another table when use encrypt feature - [#38926](https://github.com/apache/shardingsphere/pull/38926) +1. SQL Binder: Support internal binding and extraction for SQL Server UPDATE table variable targets - [#39093](https://github.com/apache/shardingsphere/pull/39093) 1. Sharding: Fix HASH_MOD routing mismatch for same negative numeric values across numeric Java types with compatibility switch `normalize-numeric-int-range` - [#38327](https://github.com/apache/shardingsphere/pull/38327) 1. Proxy Native: Support building Proxy Native via GraalVM CE for JDK 25 - [#38682](https://github.com/apache/shardingsphere/pull/38682) 1. SQL Parser: Support SQLServer table variable declaration parse - [#38904](https://github.com/apache/shardingsphere/pull/38904) diff --git a/database/connector/core/src/main/java/org/apache/shardingsphere/database/connector/core/metadata/database/metadata/DialectDatabaseMetaData.java b/database/connector/core/src/main/java/org/apache/shardingsphere/database/connector/core/metadata/database/metadata/DialectDatabaseMetaData.java index 60791671b96b0..9861978a401d3 100644 --- a/database/connector/core/src/main/java/org/apache/shardingsphere/database/connector/core/metadata/database/metadata/DialectDatabaseMetaData.java +++ b/database/connector/core/src/main/java/org/apache/shardingsphere/database/connector/core/metadata/database/metadata/DialectDatabaseMetaData.java @@ -217,4 +217,13 @@ default DialectProtocolVersionOption getProtocolVersionOption() { default DialectFunctionOption getFunctionOption() { return new DefaultFunctionOption(); } + + /** + * Get variable table name prefix. + * + * @return variable table name prefix + */ + default Optional getVariableTableNamePrefix() { + return Optional.empty(); + } } diff --git a/database/connector/dialect/sqlserver/src/main/java/org/apache/shardingsphere/database/connector/sql92/sqlserver/metadata/database/SQLServerDatabaseMetaData.java b/database/connector/dialect/sqlserver/src/main/java/org/apache/shardingsphere/database/connector/sql92/sqlserver/metadata/database/SQLServerDatabaseMetaData.java index a7b3332e2e5e9..aad3cc0d2f4f5 100644 --- a/database/connector/dialect/sqlserver/src/main/java/org/apache/shardingsphere/database/connector/sql92/sqlserver/metadata/database/SQLServerDatabaseMetaData.java +++ b/database/connector/dialect/sqlserver/src/main/java/org/apache/shardingsphere/database/connector/sql92/sqlserver/metadata/database/SQLServerDatabaseMetaData.java @@ -88,6 +88,11 @@ public Optional getAlterTableOption() { return Optional.of(new DialectAlterTableOption(false, false, false, new DialectAddColumnOption("ADD", ""))); } + @Override + public Optional getVariableTableNamePrefix() { + return Optional.of("@"); + } + @Override public String getDatabaseType() { return "SQLServer"; diff --git a/database/connector/dialect/sqlserver/src/test/java/org/apache/shardingsphere/database/connector/sql92/sqlserver/metadata/database/SQLServerDatabaseMetaDataTest.java b/database/connector/dialect/sqlserver/src/test/java/org/apache/shardingsphere/database/connector/sql92/sqlserver/metadata/database/SQLServerDatabaseMetaDataTest.java index 0fa896b4658ef..7ce40f4df37d4 100644 --- a/database/connector/dialect/sqlserver/src/test/java/org/apache/shardingsphere/database/connector/sql92/sqlserver/metadata/database/SQLServerDatabaseMetaDataTest.java +++ b/database/connector/dialect/sqlserver/src/test/java/org/apache/shardingsphere/database/connector/sql92/sqlserver/metadata/database/SQLServerDatabaseMetaDataTest.java @@ -114,4 +114,9 @@ void assertGetAlterTableOption() { void assertGetFunctionOption() { assertThat(dialectDatabaseMetaData.getFunctionOption(), isA(SQLServerFunctionOption.class)); } + + @Test + void assertGetVariableTableNamePrefix() { + assertThat(dialectDatabaseMetaData.getVariableTableNamePrefix(), is(Optional.of("@"))); + } } diff --git a/infra/binder/core/pom.xml b/infra/binder/core/pom.xml index 19243bf97e9fd..c3cc43f429ab9 100644 --- a/infra/binder/core/pom.xml +++ b/infra/binder/core/pom.xml @@ -62,5 +62,11 @@ ${project.version} test + + org.apache.shardingsphere + shardingsphere-database-connector-sqlserver + ${project.version} + test + diff --git a/infra/binder/core/src/main/java/org/apache/shardingsphere/infra/binder/engine/segment/dml/expression/type/ColumnSegmentBinder.java b/infra/binder/core/src/main/java/org/apache/shardingsphere/infra/binder/engine/segment/dml/expression/type/ColumnSegmentBinder.java index e44a8bb36619e..ea64a47b5936e 100644 --- a/infra/binder/core/src/main/java/org/apache/shardingsphere/infra/binder/engine/segment/dml/expression/type/ColumnSegmentBinder.java +++ b/infra/binder/core/src/main/java/org/apache/shardingsphere/infra/binder/engine/segment/dml/expression/type/ColumnSegmentBinder.java @@ -323,7 +323,7 @@ private static boolean isSkipColumnBind(final Collection projectionSegments, final TableSourceType tableSourceType) { columnLabelProjectionSegments = new CaseInsensitiveMap<>(projectionSegments.size(), 1F); projectionSegments.forEach(each -> putColumnLabelProjectionSegments(each, columnLabelProjectionSegments)); diff --git a/infra/binder/core/src/main/java/org/apache/shardingsphere/infra/binder/engine/segment/dml/from/type/SimpleTableSegmentBinder.java b/infra/binder/core/src/main/java/org/apache/shardingsphere/infra/binder/engine/segment/dml/from/type/SimpleTableSegmentBinder.java index cb2b4d97cf659..8414f11410497 100644 --- a/infra/binder/core/src/main/java/org/apache/shardingsphere/infra/binder/engine/segment/dml/from/type/SimpleTableSegmentBinder.java +++ b/infra/binder/core/src/main/java/org/apache/shardingsphere/infra/binder/engine/segment/dml/from/type/SimpleTableSegmentBinder.java @@ -318,6 +318,9 @@ private static void checkTableExists(final SQLStatementBinderContext binderConte if (segment.getDbLink().isPresent()) { return; } + if (isVariableTable(binderContext, segment)) { + return; + } if (binderContext.getExternalTableBinderContexts().containsKey(CaseInsensitiveString.of(tableNameValue))) { return; } @@ -327,6 +330,28 @@ private static void checkTableExists(final SQLStatementBinderContext binderConte ShardingSpherePreconditions.checkState(null != schema && schema.containsTable(tableName), () -> new TableNotFoundException(tableNameValue)); } + private static boolean isVariableTable(final SQLStatementBinderContext binderContext, final SimpleTableSegment segment) { + if (!(binderContext.getSqlStatement() instanceof UpdateStatement)) { + return false; + } + if (segment.getOwner().isPresent()) { + return false; + } + IdentifierValue tableName = segment.getTableName().getIdentifier(); + if (QuoteCharacter.NONE != tableName.getQuoteCharacter()) { + return false; + } + DialectDatabaseMetaData dialectDatabaseMetaData = new DatabaseTypeRegistry(binderContext.getSqlStatement().getDatabaseType()).getDialectDatabaseMetaData(); + return dialectDatabaseMetaData.getVariableTableNamePrefix().filter(tableName.getValue()::startsWith).isPresent(); + } + + private static boolean isUpdateTargetTableVariable(final SQLStatementBinderContext binderContext, final SimpleTableSegment segment) { + if (!isVariableTable(binderContext, segment)) { + return false; + } + return ((UpdateStatement) binderContext.getSqlStatement()).getTable() == segment; + } + private static boolean isCreateTable(final SimpleTableSegment simpleTableSegment, final String tableName) { return simpleTableSegment.getTableName().getIdentifier().getValue().equalsIgnoreCase(tableName); } @@ -379,6 +404,11 @@ private static void checkTableMetadata(final SQLStatementBinderContext binderCon private static Optional createSimpleTableBinderContext(final SimpleTableSegment segment, final ShardingSphereSchema schema, final IdentifierValue databaseName, final IdentifierValue schemaName, final SQLStatementBinderContext binderContext) { IdentifierValue tableName = segment.getTableName().getIdentifier(); + if (isUpdateTargetTableVariable(binderContext, segment)) { + SimpleTableSegmentBinderContext result = new SimpleTableSegmentBinderContext(Collections.emptyList(), TableSourceType.TEMPORARY_TABLE); + result.setContainsTableVariable(true); + return Optional.of(result); + } Optional externalTableBinderContext = createExternalTableBinderContext(segment, tableName, binderContext); if (externalTableBinderContext.isPresent()) { return externalTableBinderContext; @@ -392,6 +422,9 @@ private static Optional createSimpleTableBinder } SimpleTableSegmentBinderContext result = new SimpleTableSegmentBinderContext(Collections.emptyList(), TableSourceType.TEMPORARY_TABLE); segment.getDbLink().ifPresent(optional -> result.setContainsDBLink(true)); + if (isVariableTable(binderContext, segment)) { + result.setContainsTableVariable(true); + } return Optional.of(result); } diff --git a/infra/binder/core/src/test/java/org/apache/shardingsphere/infra/binder/context/statement/type/dml/UpdateStatementContextTest.java b/infra/binder/core/src/test/java/org/apache/shardingsphere/infra/binder/context/statement/type/dml/UpdateStatementContextTest.java index 76013af938e35..a98a9a2441223 100644 --- a/infra/binder/core/src/test/java/org/apache/shardingsphere/infra/binder/context/statement/type/dml/UpdateStatementContextTest.java +++ b/infra/binder/core/src/test/java/org/apache/shardingsphere/infra/binder/context/statement/type/dml/UpdateStatementContextTest.java @@ -17,6 +17,7 @@ package org.apache.shardingsphere.infra.binder.context.statement.type.dml; +import org.apache.shardingsphere.database.connector.core.metadata.database.enums.QuoteCharacter; import org.apache.shardingsphere.database.connector.core.type.DatabaseType; import org.apache.shardingsphere.infra.spi.type.typed.TypedSPILoader; import org.apache.shardingsphere.sql.parser.statement.core.segment.dml.assignment.SetAssignmentSegment; @@ -52,6 +53,8 @@ class UpdateStatementContextTest { private final DatabaseType databaseType = TypedSPILoader.getService(DatabaseType.class, "FIXTURE"); + private final DatabaseType sqlServerDatabaseType = TypedSPILoader.getService(DatabaseType.class, "SQLServer"); + @Mock private WhereSegment whereSegment; @@ -95,6 +98,35 @@ void assertGetTableNamesWithSQLServerUpdateAliasTargetExcludesAlias() { assertFalse(actual.getTablesContext().getTableNames().contains("sr")); } + @Test + void assertGetTableNamesWithSQLServerUpdateTableVariableTargetExcludesVariableTable() { + SimpleTableSegment fromTable = new SimpleTableSegment(new TableNameSegment(0, 0, new IdentifierValue("Employee"))); + fromTable.setOwner(new OwnerSegment(0, 0, new IdentifierValue("HumanResources"))); + UpdateStatement updateStatement = UpdateStatement.builder() + .databaseType(sqlServerDatabaseType) + .table(new SimpleTableSegment(new TableNameSegment(0, 0, new IdentifierValue("@MyTableVar")))) + .from(fromTable) + .setAssignment(new SetAssignmentSegment(0, 0, Collections.emptyList())) + .build(); + UpdateStatementContext actual = new UpdateStatementContext(updateStatement); + assertThat(actual.getTablesContext().getTableNames(), is(Collections.singleton("Employee"))); + assertFalse(actual.getTablesContext().getTableNames().contains("@MyTableVar")); + assertThat(((SimpleTableSegment) actual.getSqlStatement().getTable()).getTableName().getIdentifier().getValue(), is("@MyTableVar")); + } + + @Test + void assertGetTableNamesWithSQLServerBracketDelimitedAtSignTargetIncludesPhysicalTable() { + SimpleTableSegment fromTable = new SimpleTableSegment(new TableNameSegment(0, 0, new IdentifierValue("Employee"))); + UpdateStatement updateStatement = UpdateStatement.builder() + .databaseType(sqlServerDatabaseType) + .table(new SimpleTableSegment(new TableNameSegment(0, 0, new IdentifierValue("@MyTable", QuoteCharacter.BRACKETS)))) + .from(fromTable) + .setAssignment(new SetAssignmentSegment(0, 0, Collections.emptyList())) + .build(); + UpdateStatementContext actual = new UpdateStatementContext(updateStatement); + assertThat(actual.getTablesContext().getTableNames(), is(new HashSet<>(Arrays.asList("@MyTable", "Employee")))); + } + @Test void assertGetTableNamesWithPostgreSQLUpdateFromClauseIncludesTargetTable() { SimpleTableSegment targetTable = new SimpleTableSegment(new TableNameSegment(7, 18, new IdentifierValue("ScrapReason"))); diff --git a/infra/binder/core/src/test/java/org/apache/shardingsphere/infra/binder/engine/segment/dml/expression/type/ColumnSegmentBinderTest.java b/infra/binder/core/src/test/java/org/apache/shardingsphere/infra/binder/engine/segment/dml/expression/type/ColumnSegmentBinderTest.java index a8c6b3067ff9e..419a365a57833 100644 --- a/infra/binder/core/src/test/java/org/apache/shardingsphere/infra/binder/engine/segment/dml/expression/type/ColumnSegmentBinderTest.java +++ b/infra/binder/core/src/test/java/org/apache/shardingsphere/infra/binder/engine/segment/dml/expression/type/ColumnSegmentBinderTest.java @@ -25,6 +25,7 @@ import org.apache.shardingsphere.infra.binder.engine.segment.dml.from.context.TableSegmentBinderContext; import org.apache.shardingsphere.infra.binder.engine.segment.dml.from.context.type.SimpleTableSegmentBinderContext; import org.apache.shardingsphere.infra.binder.engine.statement.SQLStatementBinderContext; +import org.apache.shardingsphere.infra.exception.kernel.metadata.ColumnNotFoundException; import org.apache.shardingsphere.infra.exception.kernel.syntax.AmbiguousColumnException; import org.apache.shardingsphere.infra.hint.HintValueContext; import org.apache.shardingsphere.infra.metadata.ShardingSphereMetaData; @@ -238,4 +239,25 @@ void assertBindExcludedColumnInSetAssignment() { assertTrue(actual.getOwner().isPresent()); assertThat(actual.getOwner().get().getIdentifier().getValue(), is("EXCLUDED")); } + + @Test + void assertBindWithTableVariableSkipsColumnExistenceCheck() { + Multimap tableBinderContexts = LinkedHashMultimap.create(); + SimpleTableSegmentBinderContext tableVariableBinderContext = new SimpleTableSegmentBinderContext(Collections.emptyList(), TableSourceType.TEMPORARY_TABLE); + tableVariableBinderContext.setContainsTableVariable(true); + tableBinderContexts.put(CaseInsensitiveString.of("@MyTableVar"), tableVariableBinderContext); + ColumnSegment columnSegment = new ColumnSegment(0, 0, new IdentifierValue("NewVacationHours")); + ColumnSegment actual = ColumnSegmentBinder.bind(columnSegment, SegmentType.SET_ASSIGNMENT, createBinderContext(), tableBinderContexts, LinkedHashMultimap.create()); + assertThat(actual.getColumnBoundInfo().getOriginalColumn().getValue(), is("NewVacationHours")); + } + + @Test + void assertBindUnknownColumnWithoutTableVariable() { + Multimap tableBinderContexts = LinkedHashMultimap.create(); + tableBinderContexts.put(CaseInsensitiveString.of("t_order"), + new SimpleTableSegmentBinderContext(Collections.emptyList(), TableSourceType.PHYSICAL_TABLE)); + ColumnSegment columnSegment = new ColumnSegment(0, 0, new IdentifierValue("NewVacationHours")); + assertThrows(ColumnNotFoundException.class, + () -> ColumnSegmentBinder.bind(columnSegment, SegmentType.SET_ASSIGNMENT, createBinderContext(), tableBinderContexts, LinkedHashMultimap.create())); + } } diff --git a/infra/binder/core/src/test/java/org/apache/shardingsphere/infra/binder/engine/segment/dml/from/type/SimpleTableSegmentBinderTest.java b/infra/binder/core/src/test/java/org/apache/shardingsphere/infra/binder/engine/segment/dml/from/type/SimpleTableSegmentBinderTest.java index 9abd5895883f4..7941735c501e5 100644 --- a/infra/binder/core/src/test/java/org/apache/shardingsphere/infra/binder/engine/segment/dml/from/type/SimpleTableSegmentBinderTest.java +++ b/infra/binder/core/src/test/java/org/apache/shardingsphere/infra/binder/engine/segment/dml/from/type/SimpleTableSegmentBinderTest.java @@ -20,6 +20,7 @@ import com.cedarsoftware.util.CaseInsensitiveMap.CaseInsensitiveString; import com.google.common.collect.LinkedHashMultimap; import com.google.common.collect.Multimap; +import org.apache.shardingsphere.database.connector.core.metadata.database.enums.QuoteCharacter; import org.apache.shardingsphere.database.connector.core.type.DatabaseType; import org.apache.shardingsphere.infra.binder.engine.segment.dml.from.context.TableSegmentBinderContext; import org.apache.shardingsphere.infra.binder.engine.segment.dml.from.context.type.SimpleTableSegmentBinderContext; @@ -30,11 +31,14 @@ import org.apache.shardingsphere.infra.metadata.database.schema.model.ShardingSphereColumn; import org.apache.shardingsphere.infra.metadata.database.schema.model.ShardingSphereSchema; import org.apache.shardingsphere.infra.spi.type.typed.TypedSPILoader; +import org.apache.shardingsphere.sql.parser.statement.core.enums.TableSourceType; +import org.apache.shardingsphere.sql.parser.statement.core.segment.dml.assignment.SetAssignmentSegment; import org.apache.shardingsphere.sql.parser.statement.core.segment.dml.column.ColumnSegment; import org.apache.shardingsphere.sql.parser.statement.core.segment.generic.OwnerSegment; import org.apache.shardingsphere.sql.parser.statement.core.segment.generic.table.SimpleTableSegment; import org.apache.shardingsphere.sql.parser.statement.core.segment.generic.table.TableNameSegment; import org.apache.shardingsphere.sql.parser.statement.core.statement.type.dml.SelectStatement; +import org.apache.shardingsphere.sql.parser.statement.core.statement.type.dml.UpdateStatement; import org.apache.shardingsphere.sql.parser.statement.core.value.identifier.IdentifierValue; import org.junit.jupiter.api.Test; @@ -56,6 +60,8 @@ class SimpleTableSegmentBinderTest { private final DatabaseType hiveDatabaseType = TypedSPILoader.getService(DatabaseType.class, "Hive"); + private final DatabaseType sqlServerDatabaseType = TypedSPILoader.getService(DatabaseType.class, "SQLServer"); + @SuppressWarnings("resource") @Test void assertBindTableNotExists() { @@ -78,6 +84,75 @@ void assertBindWithDBLinkContainsDBLink() { assertTrue(tableSegmentBinderContext.isContainsDBLink()); } + @Test + void assertBindWithTableVariableContainsTableVariable() { + SimpleTableSegment tableVar = new SimpleTableSegment(new TableNameSegment(0, 10, new IdentifierValue("@MyTableVar"))); + UpdateStatement updateStatement = UpdateStatement.builder() + .databaseType(sqlServerDatabaseType) + .table(tableVar) + .setAssignment(new SetAssignmentSegment(0, 0, Collections.emptyList())) + .build(); + Multimap tableBinderContexts = LinkedHashMultimap.create(); + SimpleTableSegmentBinder.bind(tableVar, new SQLStatementBinderContext(createMetaData(), "foo_db", new HintValueContext(), updateStatement), tableBinderContexts); + SimpleTableSegmentBinderContext tableSegmentBinderContext = (SimpleTableSegmentBinderContext) tableBinderContexts.values().iterator().next(); + assertTrue(tableSegmentBinderContext.isContainsTableVariable()); + } + + @Test + void assertBindWithUpdateTargetTableVariablePrecedesSameNamedPhysicalTable() { + SimpleTableSegment tableVar = new SimpleTableSegment(new TableNameSegment(0, 10, new IdentifierValue("@MyTableVar"))); + UpdateStatement updateStatement = UpdateStatement.builder() + .databaseType(sqlServerDatabaseType) + .table(tableVar) + .setAssignment(new SetAssignmentSegment(0, 0, Collections.emptyList())) + .build(); + Multimap tableBinderContexts = LinkedHashMultimap.create(); + SimpleTableSegmentBinder.bind(tableVar, new SQLStatementBinderContext(createMetaDataWithAtSignTable(), "foo_db", new HintValueContext(), updateStatement), tableBinderContexts); + SimpleTableSegmentBinderContext tableSegmentBinderContext = (SimpleTableSegmentBinderContext) tableBinderContexts.values().iterator().next(); + assertTrue(tableSegmentBinderContext.isContainsTableVariable()); + } + + @Test + void assertBindWithUpdateTargetTableVariablePrecedesSameNamedCTE() { + SimpleTableSegment tableVar = new SimpleTableSegment(new TableNameSegment(0, 10, new IdentifierValue("@MyTableVar"))); + UpdateStatement updateStatement = UpdateStatement.builder() + .databaseType(sqlServerDatabaseType) + .table(tableVar) + .setAssignment(new SetAssignmentSegment(0, 0, Collections.emptyList())) + .build(); + SQLStatementBinderContext binderContext = new SQLStatementBinderContext(createMetaData(), "foo_db", new HintValueContext(), updateStatement); + binderContext.getExternalTableBinderContexts().put(CaseInsensitiveString.of("@MyTableVar"), + new SimpleTableSegmentBinderContext(Collections.emptyList(), TableSourceType.TEMPORARY_TABLE)); + Multimap tableBinderContexts = LinkedHashMultimap.create(); + SimpleTableSegmentBinder.bind(tableVar, binderContext, tableBinderContexts); + SimpleTableSegmentBinderContext tableSegmentBinderContext = (SimpleTableSegmentBinderContext) tableBinderContexts.values().iterator().next(); + assertTrue(tableSegmentBinderContext.isContainsTableVariable()); + } + + @Test + void assertBindWithAtSignTableInSelectThrowsTableNotFoundException() { + SimpleTableSegment simpleTableSegment = new SimpleTableSegment(new TableNameSegment(0, 10, new IdentifierValue("@MyTableVar"))); + Multimap tableBinderContexts = LinkedHashMultimap.create(); + assertThrows(TableNotFoundException.class, () -> SimpleTableSegmentBinder.bind(simpleTableSegment, + new SQLStatementBinderContext(createMetaData(), "foo_db", new HintValueContext(), SelectStatement.builder().databaseType(sqlServerDatabaseType).build()), tableBinderContexts)); + } + + @Test + void assertBindWithBracketDelimitedAtSignTableNameIsNotTableVariable() { + SimpleTableSegment simpleTableSegment = new SimpleTableSegment(new TableNameSegment(0, 10, new IdentifierValue("@MyTable", QuoteCharacter.BRACKETS))); + Multimap tableBinderContexts = LinkedHashMultimap.create(); + assertThrows(TableNotFoundException.class, () -> SimpleTableSegmentBinder.bind(simpleTableSegment, + new SQLStatementBinderContext(createMetaData(), "foo_db", new HintValueContext(), SelectStatement.builder().databaseType(sqlServerDatabaseType).build()), tableBinderContexts)); + } + + @Test + void assertBindWithQuoteDelimitedAtSignTableNameIsNotTableVariable() { + SimpleTableSegment simpleTableSegment = new SimpleTableSegment(new TableNameSegment(0, 10, new IdentifierValue("@MyTable", QuoteCharacter.QUOTE))); + Multimap tableBinderContexts = LinkedHashMultimap.create(); + assertThrows(TableNotFoundException.class, () -> SimpleTableSegmentBinder.bind(simpleTableSegment, + new SQLStatementBinderContext(createMetaData(), "foo_db", new HintValueContext(), SelectStatement.builder().databaseType(sqlServerDatabaseType).build()), tableBinderContexts)); + } + @Test void assertBindTableSampleExpression() { SimpleTableSegment simpleTableSegment = new SimpleTableSegment(new TableNameSegment(0, 6, new IdentifierValue("t_order"))); @@ -157,6 +232,27 @@ private ShardingSphereMetaData createMetaData() { return result; } + private ShardingSphereMetaData createMetaDataWithAtSignTable() { + ShardingSphereSchema schema = mock(ShardingSphereSchema.class, RETURNS_DEEP_STUBS); + IdentifierValue fooDatabase = new IdentifierValue("foo_db"); + IdentifierValue dboSchema = new IdentifierValue("dbo"); + IdentifierValue atSignTableName = new IdentifierValue("@MyTableVar"); + when(schema.getName()).thenReturn("dbo"); + when(schema.containsTable(atSignTableName)).thenReturn(true); + when(schema.getTable(atSignTableName).getAllColumns()).thenReturn(Collections.emptyList()); + ShardingSphereMetaData result = mock(ShardingSphereMetaData.class, RETURNS_DEEP_STUBS); + when(result.containsDatabase(fooDatabase)).thenReturn(true); + when(result.getDatabase("foo_db").getDefaultSchemaName()).thenReturn("dbo"); + when(result.getDatabase(fooDatabase).getDefaultSchemaName()).thenReturn("dbo"); + when(result.getDatabase("foo_db").getAllSchemas()).thenReturn(Collections.singleton(schema)); + when(result.getDatabase(fooDatabase).getAllSchemas()).thenReturn(Collections.singleton(schema)); + when(result.getDatabase("foo_db").containsSchema("dbo")).thenReturn(true); + when(result.getDatabase(fooDatabase).containsSchema(dboSchema)).thenReturn(true); + when(result.getDatabase("foo_db").getSchema("dbo")).thenReturn(schema); + when(result.getDatabase(fooDatabase).getSchema(dboSchema)).thenReturn(schema); + return result; + } + private ShardingSphereMetaData createHiveMetaDataWithLoadedSchema() { ShardingSphereSchema schema = mock(ShardingSphereSchema.class, RETURNS_DEEP_STUBS); IdentifierValue fooDatabase = new IdentifierValue("foo_db"); diff --git a/infra/binder/core/src/test/java/org/apache/shardingsphere/infra/binder/engine/statement/dml/UpdateStatementBinderTest.java b/infra/binder/core/src/test/java/org/apache/shardingsphere/infra/binder/engine/statement/dml/UpdateStatementBinderTest.java index 0dbdbcaf75f3a..ecc04fbbed99d 100644 --- a/infra/binder/core/src/test/java/org/apache/shardingsphere/infra/binder/engine/statement/dml/UpdateStatementBinderTest.java +++ b/infra/binder/core/src/test/java/org/apache/shardingsphere/infra/binder/engine/statement/dml/UpdateStatementBinderTest.java @@ -18,6 +18,7 @@ package org.apache.shardingsphere.infra.binder.engine.statement.dml; import org.apache.shardingsphere.database.connector.core.type.DatabaseType; +import org.apache.shardingsphere.infra.binder.context.statement.type.dml.UpdateStatementContext; import org.apache.shardingsphere.infra.binder.engine.statement.SQLStatementBinderContext; import org.apache.shardingsphere.infra.hint.HintValueContext; import org.apache.shardingsphere.infra.metadata.ShardingSphereMetaData; @@ -56,6 +57,7 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.isA; import static org.hamcrest.Matchers.not; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.RETURNS_DEEP_STUBS; import static org.mockito.Mockito.mock; @@ -65,6 +67,8 @@ class UpdateStatementBinderTest { private final DatabaseType databaseType = TypedSPILoader.getService(DatabaseType.class, "FIXTURE"); + private final DatabaseType sqlServerDatabaseType = TypedSPILoader.getService(DatabaseType.class, "SQLServer"); + @Test void assertBind() { SimpleTableSegment simpleTableSegment = new SimpleTableSegment(new TableNameSegment(0, 0, new IdentifierValue("t_order"))); @@ -174,6 +178,35 @@ void assertBindSchemaQualifiedUpdateTargetTableAlias() { assertTrue(actual.isTargetTableIsFromAlias()); } + @Test + void assertBindUpdateTableVariableTarget() { + SimpleTableSegment fromTable = new SimpleTableSegment(new TableNameSegment(0, 0, new IdentifierValue("Employee"))); + fromTable.setOwner(new OwnerSegment(0, 0, new IdentifierValue("HumanResources"))); + fromTable.setAlias(new AliasSegment(0, 0, new IdentifierValue("e"))); + ColumnSegment setColumn = new ColumnSegment(0, 0, new IdentifierValue("NewVacationHours")); + ColumnSegment fromColumn = new ColumnSegment(0, 0, new IdentifierValue("VacationHours")); + fromColumn.setOwner(new OwnerSegment(0, 0, new IdentifierValue("e"))); + UpdateStatement updateStatement = UpdateStatement.builder() + .databaseType(sqlServerDatabaseType) + .table(new SimpleTableSegment(new TableNameSegment(0, 0, new IdentifierValue("@MyTableVar")))) + .from(fromTable) + .setAssignment(new SetAssignmentSegment(0, 0, Collections.singletonList(new ColumnAssignmentSegment(0, 0, Collections.singletonList(setColumn), + new BinaryOperationExpression(0, 0, fromColumn, new LiteralExpressionSegment(0, 0, 20), "+", "e.VacationHours + 20"))))) + .build(); + UpdateStatement actual = new UpdateStatementBinder().bind(updateStatement, + new SQLStatementBinderContext(createSQLServerMetaData(), "foo_db", new HintValueContext(), updateStatement)); + ColumnSegment actualSetColumn = actual.getAssignment().get().getAssignments().iterator().next().getColumns().iterator().next(); + BinaryOperationExpression actualValue = (BinaryOperationExpression) actual.getAssignment().get().getAssignments().iterator().next().getValue(); + assertThat(((SimpleTableSegment) actual.getTable()).getTableName().getIdentifier().getValue(), is("@MyTableVar")); + assertThat(actualSetColumn.getColumnBoundInfo().getOriginalColumn().getValue(), is("NewVacationHours")); + assertThat(((ColumnSegment) actualValue.getLeft()).getColumnBoundInfo().getOriginalTable().getValue(), is("Employee")); + assertThat(((ColumnSegment) actualValue.getLeft()).getColumnBoundInfo().getOriginalColumn().getValue(), is("VacationHours")); + UpdateStatementContext updateStatementContext = new UpdateStatementContext(actual); + assertThat(((SimpleTableSegment) updateStatementContext.getSqlStatement().getTable()).getTableName().getIdentifier().getValue(), is("@MyTableVar")); + assertThat(updateStatementContext.getTablesContext().getTableNames(), is(Collections.singleton("Employee"))); + assertFalse(updateStatementContext.getTablesContext().getTableNames().contains("@MyTableVar")); + } + private WithSegment createWithSegment() { return new WithSegment(0, 0, new LinkedList<>(Collections.singletonList( new CommonTableExpressionSegment(0, 0, new AliasSegment(0, 0, new IdentifierValue("combined_users")), @@ -220,4 +253,36 @@ private ShardingSphereMetaData createMetaData() { when(result.getDatabase(fooDatabase).getSchema(fooDatabase).containsTable(tUser)).thenReturn(true); return result; } + + private ShardingSphereMetaData createSQLServerMetaData() { + ShardingSphereSchema humanResourcesSchema = mock(ShardingSphereSchema.class, RETURNS_DEEP_STUBS); + IdentifierValue fooDatabase = new IdentifierValue("foo_db"); + IdentifierValue humanResources = new IdentifierValue("HumanResources"); + IdentifierValue employee = new IdentifierValue("Employee"); + when(humanResourcesSchema.getName()).thenReturn("HumanResources"); + when(humanResourcesSchema.containsTable(employee)).thenReturn(true); + when(humanResourcesSchema.containsTable("Employee")).thenReturn(true); + when(humanResourcesSchema.getTable(employee).getAllColumns()).thenReturn(Arrays.asList( + new ShardingSphereColumn("BusinessEntityID", Types.INTEGER, false, false, false, true, false, false), + new ShardingSphereColumn("VacationHours", Types.INTEGER, false, false, false, true, false, false), + new ShardingSphereColumn("VacationNote", Types.VARCHAR, false, false, false, true, false, false))); + when(humanResourcesSchema.getTable("Employee").getAllColumns()).thenReturn(Arrays.asList( + new ShardingSphereColumn("BusinessEntityID", Types.INTEGER, false, false, false, true, false, false), + new ShardingSphereColumn("VacationHours", Types.INTEGER, false, false, false, true, false, false), + new ShardingSphereColumn("VacationNote", Types.VARCHAR, false, false, false, true, false, false))); + ShardingSphereMetaData result = mock(ShardingSphereMetaData.class, RETURNS_DEEP_STUBS); + when(result.containsDatabase("foo_db")).thenReturn(true); + when(result.containsDatabase(fooDatabase)).thenReturn(true); + when(result.getDatabase("foo_db").getDefaultSchemaName()).thenReturn("dbo"); + when(result.getDatabase(fooDatabase).getDefaultSchemaName()).thenReturn("dbo"); + when(result.getDatabase("foo_db").containsSchema("dbo")).thenReturn(true); + when(result.getDatabase(fooDatabase).containsSchema(new IdentifierValue("dbo"))).thenReturn(true); + when(result.getDatabase("foo_db").containsSchema("HumanResources")).thenReturn(true); + when(result.getDatabase(fooDatabase).containsSchema(humanResources)).thenReturn(true); + when(result.getDatabase("foo_db").getSchema("HumanResources")).thenReturn(humanResourcesSchema); + when(result.getDatabase(fooDatabase).getSchema(humanResources)).thenReturn(humanResourcesSchema); + when(result.getDatabase("foo_db").getAllSchemas()).thenReturn(Collections.singleton(humanResourcesSchema)); + when(result.getDatabase(fooDatabase).getAllSchemas()).thenReturn(Collections.singleton(humanResourcesSchema)); + return result; + } } diff --git a/kernel/sql-federation/compiler/src/test/java/org/apache/shardingsphere/sqlfederation/compiler/sql/ast/converter/statement/type/UpdateStatementConverterTest.java b/kernel/sql-federation/compiler/src/test/java/org/apache/shardingsphere/sqlfederation/compiler/sql/ast/converter/statement/type/UpdateStatementConverterTest.java index ca406e31f358c..7fa2518a21447 100644 --- a/kernel/sql-federation/compiler/src/test/java/org/apache/shardingsphere/sqlfederation/compiler/sql/ast/converter/statement/type/UpdateStatementConverterTest.java +++ b/kernel/sql-federation/compiler/src/test/java/org/apache/shardingsphere/sqlfederation/compiler/sql/ast/converter/statement/type/UpdateStatementConverterTest.java @@ -55,6 +55,20 @@ class UpdateStatementConverterTest { private final DatabaseType databaseType = TypedSPILoader.getService(DatabaseType.class, "FIXTURE"); + private final DatabaseType sqlServerDatabaseType = TypedSPILoader.getService(DatabaseType.class, "SQLServer"); + + @Test + void assertConvertWithTableVariableTarget() { + SimpleTableSegment tableSegment = new SimpleTableSegment(new TableNameSegment(0, 0, new IdentifierValue("@MyTableVar"))); + UpdateStatement updateStatement = UpdateStatement.builder() + .databaseType(sqlServerDatabaseType) + .table(tableSegment) + .setAssignment(createSetAssignmentSegment()) + .build(); + SqlUpdate actual = (SqlUpdate) new UpdateStatementConverter().convert(updateStatement); + assertThat(((SqlIdentifier) actual.getTargetTable()).getSimple(), is("@MyTableVar")); + } + @Test void assertConvertWithLimitAndAlias() { LimitSegment limit = new LimitSegment(0, 0, new NumberLiteralLimitValueSegment(0, 0, 1L), new ParameterMarkerLimitValueSegment(0, 0, 0)); diff --git a/parser/sql/statement/core/pom.xml b/parser/sql/statement/core/pom.xml index 40da880025dbd..78eb6c64c1f3c 100644 --- a/parser/sql/statement/core/pom.xml +++ b/parser/sql/statement/core/pom.xml @@ -39,6 +39,12 @@ ${project.version} test + + org.apache.shardingsphere + shardingsphere-database-connector-sqlserver + ${project.version} + test + org.apache.groovy diff --git a/parser/sql/statement/core/src/main/java/org/apache/shardingsphere/sql/parser/statement/core/extractor/TableExtractor.java b/parser/sql/statement/core/src/main/java/org/apache/shardingsphere/sql/parser/statement/core/extractor/TableExtractor.java index 208f59f9bdffb..a2d60f8270cd9 100644 --- a/parser/sql/statement/core/src/main/java/org/apache/shardingsphere/sql/parser/statement/core/extractor/TableExtractor.java +++ b/parser/sql/statement/core/src/main/java/org/apache/shardingsphere/sql/parser/statement/core/extractor/TableExtractor.java @@ -18,6 +18,9 @@ package org.apache.shardingsphere.sql.parser.statement.core.extractor; import lombok.Getter; +import org.apache.shardingsphere.database.connector.core.metadata.database.enums.QuoteCharacter; +import org.apache.shardingsphere.database.connector.core.metadata.database.metadata.DialectDatabaseMetaData; +import org.apache.shardingsphere.database.connector.core.type.DatabaseTypeRegistry; import org.apache.shardingsphere.sql.parser.statement.core.segment.ddl.routine.RoutineBodySegment; import org.apache.shardingsphere.sql.parser.statement.core.segment.ddl.routine.ValidStatementSegment; import org.apache.shardingsphere.sql.parser.statement.core.segment.dml.assignment.ColumnAssignmentSegment; @@ -69,6 +72,7 @@ import org.apache.shardingsphere.sql.parser.statement.core.statement.type.dml.InsertStatement; import org.apache.shardingsphere.sql.parser.statement.core.statement.type.dml.SelectStatement; import org.apache.shardingsphere.sql.parser.statement.core.statement.type.dml.UpdateStatement; +import org.apache.shardingsphere.sql.parser.statement.core.value.identifier.IdentifierValue; import java.util.Collection; import java.util.Collections; @@ -313,7 +317,7 @@ private void extractTablesFromColumnSegments(final Collection col * @param updateStatement update statement. */ public void extractTablesFromUpdate(final UpdateStatement updateStatement) { - if (!updateStatement.isTargetTableIsFromAlias()) { + if (!updateStatement.isTargetTableIsFromAlias() && !isVariableTableTarget(updateStatement)) { extractTablesFromTableSegment(updateStatement.getTable()); } updateStatement.getFrom().ifPresent(this::extractTablesFromTableSegment); @@ -323,6 +327,22 @@ public void extractTablesFromUpdate(final UpdateStatement updateStatement) { } } + private boolean isVariableTableTarget(final UpdateStatement updateStatement) { + if (!(updateStatement.getTable() instanceof SimpleTableSegment)) { + return false; + } + SimpleTableSegment targetTable = (SimpleTableSegment) updateStatement.getTable(); + if (targetTable.getOwner().isPresent()) { + return false; + } + IdentifierValue tableName = targetTable.getTableName().getIdentifier(); + if (QuoteCharacter.NONE != tableName.getQuoteCharacter()) { + return false; + } + DialectDatabaseMetaData dialectDatabaseMetaData = new DatabaseTypeRegistry(updateStatement.getDatabaseType()).getDialectDatabaseMetaData(); + return dialectDatabaseMetaData.getVariableTableNamePrefix().filter(tableName.getValue()::startsWith).isPresent(); + } + /** * Check if the table needs to be overwritten. * diff --git a/parser/sql/statement/core/src/test/java/org/apache/shardingsphere/sql/parser/statement/core/extractor/TableExtractorTest.java b/parser/sql/statement/core/src/test/java/org/apache/shardingsphere/sql/parser/statement/core/extractor/TableExtractorTest.java index d326cf2955e01..4b0356e1a0832 100644 --- a/parser/sql/statement/core/src/test/java/org/apache/shardingsphere/sql/parser/statement/core/extractor/TableExtractorTest.java +++ b/parser/sql/statement/core/src/test/java/org/apache/shardingsphere/sql/parser/statement/core/extractor/TableExtractorTest.java @@ -21,7 +21,11 @@ import org.apache.shardingsphere.sql.parser.statement.core.enums.CombineType; import org.apache.shardingsphere.sql.parser.statement.core.segment.ddl.routine.RoutineBodySegment; import org.apache.shardingsphere.sql.parser.statement.core.segment.ddl.routine.ValidStatementSegment; +import org.apache.shardingsphere.database.connector.core.metadata.database.enums.QuoteCharacter; +import org.apache.shardingsphere.database.connector.core.type.DatabaseType; +import org.apache.shardingsphere.infra.spi.type.typed.TypedSPILoader; import org.apache.shardingsphere.sql.parser.statement.core.segment.dml.assignment.ColumnAssignmentSegment; +import org.apache.shardingsphere.sql.parser.statement.core.segment.dml.assignment.SetAssignmentSegment; import org.apache.shardingsphere.sql.parser.statement.core.segment.dml.column.ColumnSegment; import org.apache.shardingsphere.sql.parser.statement.core.segment.dml.column.OnDuplicateKeyColumnsSegment; import org.apache.shardingsphere.sql.parser.statement.core.segment.dml.combine.CombineSegment; @@ -51,6 +55,7 @@ import org.apache.shardingsphere.sql.parser.statement.core.statement.type.ddl.table.CreateTableStatement; import org.apache.shardingsphere.sql.parser.statement.core.statement.type.dml.InsertStatement; import org.apache.shardingsphere.sql.parser.statement.core.statement.type.dml.SelectStatement; +import org.apache.shardingsphere.sql.parser.statement.core.statement.type.dml.UpdateStatement; import org.apache.shardingsphere.sql.parser.statement.core.value.identifier.IdentifierValue; import org.junit.jupiter.api.Test; @@ -62,6 +67,7 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -146,6 +152,42 @@ void assertNotExistTableFromRoutineBody() { assertThat(nonExistingTables.size(), is(1)); } + @Test + void assertExtractTablesFromUpdateWithTableVariableTargetExcludesVariableTable() { + DatabaseType sqlServerDatabaseType = TypedSPILoader.getService(DatabaseType.class, "SQLServer"); + SimpleTableSegment fromTable = new SimpleTableSegment(new TableNameSegment(0, 0, new IdentifierValue("Employee"))); + fromTable.setOwner(new OwnerSegment(0, 0, new IdentifierValue("HumanResources"))); + UpdateStatement updateStatement = UpdateStatement.builder() + .databaseType(sqlServerDatabaseType) + .table(new SimpleTableSegment(new TableNameSegment(0, 0, new IdentifierValue("@MyTableVar")))) + .from(fromTable) + .setAssignment(new SetAssignmentSegment(0, 0, Collections.emptyList())) + .build(); + tableExtractor.extractTablesFromUpdate(updateStatement); + Collection actual = tableExtractor.getRewriteTables(); + assertThat(actual.size(), is(1)); + assertTableSegment(actual.iterator().next(), 0, 0, "Employee"); + assertFalse(actual.stream().anyMatch(each -> "@MyTableVar".equals(each.getTableName().getIdentifier().getValue()))); + } + + @Test + void assertExtractTablesFromUpdateWithBracketDelimitedAtSignTargetIncludesPhysicalTable() { + DatabaseType sqlServerDatabaseType = TypedSPILoader.getService(DatabaseType.class, "SQLServer"); + SimpleTableSegment fromTable = new SimpleTableSegment(new TableNameSegment(0, 0, new IdentifierValue("Employee"))); + UpdateStatement updateStatement = UpdateStatement.builder() + .databaseType(sqlServerDatabaseType) + .table(new SimpleTableSegment(new TableNameSegment(0, 0, new IdentifierValue("@MyTable", QuoteCharacter.BRACKETS)))) + .from(fromTable) + .setAssignment(new SetAssignmentSegment(0, 0, Collections.emptyList())) + .build(); + tableExtractor.extractTablesFromUpdate(updateStatement); + Collection actual = tableExtractor.getRewriteTables(); + assertThat(actual.size(), is(2)); + assertTrue(actual.stream().anyMatch(each -> "@MyTable".equals(each.getTableName().getIdentifier().getValue()) + && QuoteCharacter.BRACKETS == each.getTableName().getIdentifier().getQuoteCharacter())); + assertTrue(actual.stream().anyMatch(each -> "Employee".equals(each.getTableName().getIdentifier().getValue()))); + } + private void assertTableSegment(final SimpleTableSegment actual, final int expectedStartIndex, final int expectedStopIndex, final String expectedTableName) { assertThat(actual.getStartIndex(), is(expectedStartIndex)); assertThat(actual.getStopIndex(), is(expectedStopIndex)); diff --git a/test/it/rewriter/src/test/java/org/apache/shardingsphere/test/it/rewriter/engine/scenario/EncryptSQLRewriterIT.java b/test/it/rewriter/src/test/java/org/apache/shardingsphere/test/it/rewriter/engine/scenario/EncryptSQLRewriterIT.java index b30e44788fe7c..35c6173756986 100644 --- a/test/it/rewriter/src/test/java/org/apache/shardingsphere/test/it/rewriter/engine/scenario/EncryptSQLRewriterIT.java +++ b/test/it/rewriter/src/test/java/org/apache/shardingsphere/test/it/rewriter/engine/scenario/EncryptSQLRewriterIT.java @@ -107,6 +107,12 @@ protected Collection mockSchemas(final String schemaName) new ShardingSphereColumn("YearToDateAmt", Types.DECIMAL, false, false, false, true, false, false), new ShardingSphereColumn("RegionCode", Types.VARCHAR, false, false, false, true, false, false)), Collections.emptyList(), Collections.emptyList())); result.add(new ShardingSphereSchema("Sales", mock(DatabaseType.class), salesSchemaTables, Collections.emptyList())); + Collection humanResourcesSchemaTables = new LinkedList<>(); + humanResourcesSchemaTables.add(new ShardingSphereTable("Employee", Arrays.asList( + new ShardingSphereColumn("BusinessEntityID", Types.INTEGER, false, false, false, true, false, false), + new ShardingSphereColumn("VacationHours", Types.INTEGER, false, false, false, true, false, false), + new ShardingSphereColumn("VacationNote", Types.VARCHAR, false, false, false, true, false, false)), Collections.emptyList(), Collections.emptyList())); + result.add(new ShardingSphereSchema("HumanResources", mock(DatabaseType.class), humanResourcesSchemaTables, Collections.emptyList())); return result; } @@ -126,6 +132,7 @@ protected void mockDatabaseRules(final Collection rules, fin singleRule.get().getAttributes().getAttribute(MutableDataNodeRuleAttribute.class).put("encrypt_ds", schemaName, "SalesPerson"); singleRule.get().getAttributes().getAttribute(MutableDataNodeRuleAttribute.class).put("encrypt_ds", schemaName, "SalesOrderHeader"); singleRule.get().getAttributes().getAttribute(MutableDataNodeRuleAttribute.class).put("encrypt_ds", "Sales", "SalesPerson"); + singleRule.get().getAttributes().getAttribute(MutableDataNodeRuleAttribute.class).put("encrypt_ds", "HumanResources", "Employee"); } } } diff --git a/test/it/rewriter/src/test/resources/scenario/encrypt/case/query-with-cipher/dml/update/update.xml b/test/it/rewriter/src/test/resources/scenario/encrypt/case/query-with-cipher/dml/update/update.xml index 7c8df50c3beaf..96673d32e6694 100644 --- a/test/it/rewriter/src/test/resources/scenario/encrypt/case/query-with-cipher/dml/update/update.xml +++ b/test/it/rewriter/src/test/resources/scenario/encrypt/case/query-with-cipher/dml/update/update.xml @@ -124,4 +124,9 @@ + + + + + diff --git a/test/it/rewriter/src/test/resources/scenario/encrypt/config/query-with-cipher.yaml b/test/it/rewriter/src/test/resources/scenario/encrypt/config/query-with-cipher.yaml index 98ce1bed99047..1f8d25606887b 100644 --- a/test/it/rewriter/src/test/resources/scenario/encrypt/config/query-with-cipher.yaml +++ b/test/it/rewriter/src/test/resources/scenario/encrypt/config/query-with-cipher.yaml @@ -180,6 +180,12 @@ rules: cipher: name: commission_pct_cipher encryptorName: rewrite_normal_fixture + Employee: + columns: + VacationNote: + cipher: + name: vacation_note_cipher + encryptorName: rewrite_normal_fixture encryptors: rewrite_normal_fixture: type: REWRITE.NORMAL.FIXTURE