From 9ddda13b02d9965e63292fa1fa25104e1fee1b14 Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 14 Jul 2026 12:27:31 +0800 Subject: [PATCH 1/6] support Specifying a table variable as the target object --- .gitignore | 1 + .../metadata/DialectDatabaseMetaData.java | 9 +++ .../database/SQLServerDatabaseMetaData.java | 5 ++ .../SQLServerDatabaseMetaDataTest.java | 5 ++ infra/binder/core/pom.xml | 6 ++ .../expression/type/ColumnSegmentBinder.java | 8 ++- .../type/SimpleTableSegmentBinderContext.java | 2 + .../from/type/SimpleTableSegmentBinder.java | 14 ++++ .../type/dml/UpdateStatementContextTest.java | 19 ++++++ .../type/ColumnSegmentBinderTest.java | 22 +++++++ .../type/SimpleTableSegmentBinderTest.java | 12 ++++ .../dml/UpdateStatementBinderTest.java | 65 +++++++++++++++++++ .../type/UpdateStatementConverterTest.java | 14 ++++ parser/sql/statement/core/pom.xml | 6 ++ .../core/extractor/TableExtractor.java | 17 ++++- .../core/extractor/TableExtractorTest.java | 24 +++++++ .../engine/scenario/EncryptSQLRewriterIT.java | 7 ++ .../query-with-cipher/dml/update/update.xml | 5 ++ .../encrypt/config/query-with-cipher.yaml | 6 ++ 19 files changed, 244 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 95b71ee83d519..db59adc1bc8b5 100644 --- a/.gitignore +++ b/.gitignore @@ -76,3 +76,4 @@ document/public/ .codex/.personality_migration .antlr_last_commit +.cursor/ 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..8ea2c31bdfafd 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, tableNameValue)) { + return; + } if (binderContext.getExternalTableBinderContexts().containsKey(CaseInsensitiveString.of(tableNameValue))) { return; } @@ -327,6 +330,14 @@ 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, final String tableNameValue) { + if (segment.getOwner().isPresent()) { + return false; + } + DialectDatabaseMetaData dialectDatabaseMetaData = new DatabaseTypeRegistry(binderContext.getSqlStatement().getDatabaseType()).getDialectDatabaseMetaData(); + return dialectDatabaseMetaData.getVariableTableNamePrefix().filter(tableNameValue::startsWith).isPresent(); + } + private static boolean isCreateTable(final SimpleTableSegment simpleTableSegment, final String tableName) { return simpleTableSegment.getTableName().getIdentifier().getValue().equalsIgnoreCase(tableName); } @@ -392,6 +403,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, tableName.getValue())) { + 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..61d3ed05ac814 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 @@ -52,6 +52,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 +97,23 @@ void assertGetTableNamesWithSQLServerUpdateAliasTargetExcludesAlias() { assertFalse(actual.getTablesContext().getTableNames().contains("sr")); } + @Test + void assertGetTableNamesWithSQLServerUpdateTableVariableTargetExcludesVariableTable() { + SimpleTableSegment targetTable = new SimpleTableSegment(new TableNameSegment(0, 0, new IdentifierValue("@MyTableVar"))); + 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(targetTable) + .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 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..fa3e30fda4ba4 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 @@ -56,6 +56,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 +80,16 @@ void assertBindWithDBLinkContainsDBLink() { assertTrue(tableSegmentBinderContext.isContainsDBLink()); } + @Test + void assertBindWithTableVariableContainsTableVariable() { + SimpleTableSegment simpleTableSegment = new SimpleTableSegment(new TableNameSegment(0, 10, new IdentifierValue("@MyTableVar"))); + Multimap tableBinderContexts = LinkedHashMultimap.create(); + SimpleTableSegmentBinder.bind(simpleTableSegment, + new SQLStatementBinderContext(createMetaData(), "foo_db", new HintValueContext(), SelectStatement.builder().databaseType(sqlServerDatabaseType).build()), tableBinderContexts); + SimpleTableSegmentBinderContext tableSegmentBinderContext = (SimpleTableSegmentBinderContext) tableBinderContexts.values().iterator().next(); + assertTrue(tableSegmentBinderContext.isContainsTableVariable()); + } + @Test void assertBindTableSampleExpression() { SimpleTableSegment simpleTableSegment = new SimpleTableSegment(new TableNameSegment(0, 6, new IdentifierValue("t_order"))); 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..7611ff0bd6b47 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,8 @@ package org.apache.shardingsphere.sql.parser.statement.core.extractor; import lombok.Getter; +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; @@ -313,7 +315,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 +325,19 @@ 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; + } + String tableName = targetTable.getTableName().getIdentifier().getValue(); + DialectDatabaseMetaData dialectDatabaseMetaData = new DatabaseTypeRegistry(updateStatement.getDatabaseType()).getDialectDatabaseMetaData(); + return dialectDatabaseMetaData.getVariableTableNamePrefix().filter(tableName::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..96c042ab49013 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,10 @@ 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.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 +54,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 +66,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 +151,25 @@ void assertNotExistTableFromRoutineBody() { assertThat(nonExistingTables.size(), is(1)); } + @Test + void assertExtractTablesFromUpdateWithTableVariableTargetExcludesVariableTable() { + DatabaseType sqlServerDatabaseType = TypedSPILoader.getService(DatabaseType.class, "SQLServer"); + SimpleTableSegment targetTable = new SimpleTableSegment(new TableNameSegment(0, 0, new IdentifierValue("@MyTableVar"))); + 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(targetTable) + .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()))); + } + 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 From f395ec505f783751b6d57f38a573ae9412f128f8 Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 14 Jul 2026 13:45:24 +0800 Subject: [PATCH 2/6] support Specifying a table variable as the target object --- RELEASE-NOTES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 487a3a7b7861a..14ad3ed83b5fe 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. Encrypt: Support SqlServer update statement for Specifying a table variable as the target object when use encrypt feature - [#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) From 481389309471509462978368c495c3853df18521 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 16 Jul 2026 09:00:19 +0800 Subject: [PATCH 3/6] update --- .../from/type/SimpleTableSegmentBinder.java | 12 ++++++---- .../type/dml/UpdateStatementContextTest.java | 17 ++++++++++++-- .../type/SimpleTableSegmentBinderTest.java | 17 ++++++++++++++ .../core/extractor/TableExtractor.java | 9 ++++++-- .../core/extractor/TableExtractorTest.java | 22 +++++++++++++++++-- 5 files changed, 67 insertions(+), 10 deletions(-) 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 8ea2c31bdfafd..da2e2b38e266e 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,7 +318,7 @@ private static void checkTableExists(final SQLStatementBinderContext binderConte if (segment.getDbLink().isPresent()) { return; } - if (isVariableTable(binderContext, segment, tableNameValue)) { + if (isVariableTable(binderContext, segment)) { return; } if (binderContext.getExternalTableBinderContexts().containsKey(CaseInsensitiveString.of(tableNameValue))) { @@ -330,12 +330,16 @@ 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, final String tableNameValue) { + private static boolean isVariableTable(final SQLStatementBinderContext binderContext, final SimpleTableSegment segment) { 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(tableNameValue::startsWith).isPresent(); + return dialectDatabaseMetaData.getVariableTableNamePrefix().filter(tableName.getValue()::startsWith).isPresent(); } private static boolean isCreateTable(final SimpleTableSegment simpleTableSegment, final String tableName) { @@ -403,7 +407,7 @@ private static Optional createSimpleTableBinder } SimpleTableSegmentBinderContext result = new SimpleTableSegmentBinderContext(Collections.emptyList(), TableSourceType.TEMPORARY_TABLE); segment.getDbLink().ifPresent(optional -> result.setContainsDBLink(true)); - if (isVariableTable(binderContext, segment, tableName.getValue())) { + 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 61d3ed05ac814..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; @@ -99,12 +100,11 @@ void assertGetTableNamesWithSQLServerUpdateAliasTargetExcludesAlias() { @Test void assertGetTableNamesWithSQLServerUpdateTableVariableTargetExcludesVariableTable() { - SimpleTableSegment targetTable = new SimpleTableSegment(new TableNameSegment(0, 0, new IdentifierValue("@MyTableVar"))); 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(targetTable) + .table(new SimpleTableSegment(new TableNameSegment(0, 0, new IdentifierValue("@MyTableVar")))) .from(fromTable) .setAssignment(new SetAssignmentSegment(0, 0, Collections.emptyList())) .build(); @@ -114,6 +114,19 @@ void assertGetTableNamesWithSQLServerUpdateTableVariableTargetExcludesVariableTa 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/from/type/SimpleTableSegmentBinderTest.java b/infra/binder/core/src/test/java/org/apache/shardingsphere/infra/binder/engine/segment/dml/from/type/SimpleTableSegmentBinderTest.java index fa3e30fda4ba4..151353738959a 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; @@ -90,6 +91,22 @@ void assertBindWithTableVariableContainsTableVariable() { assertTrue(tableSegmentBinderContext.isContainsTableVariable()); } + @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"))); 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 7611ff0bd6b47..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,7 @@ 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; @@ -71,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; @@ -333,9 +335,12 @@ private boolean isVariableTableTarget(final UpdateStatement updateStatement) { if (targetTable.getOwner().isPresent()) { return false; } - String tableName = targetTable.getTableName().getIdentifier().getValue(); + IdentifierValue tableName = targetTable.getTableName().getIdentifier(); + if (QuoteCharacter.NONE != tableName.getQuoteCharacter()) { + return false; + } DialectDatabaseMetaData dialectDatabaseMetaData = new DatabaseTypeRegistry(updateStatement.getDatabaseType()).getDialectDatabaseMetaData(); - return dialectDatabaseMetaData.getVariableTableNamePrefix().filter(tableName::startsWith).isPresent(); + return dialectDatabaseMetaData.getVariableTableNamePrefix().filter(tableName.getValue()::startsWith).isPresent(); } /** 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 96c042ab49013..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,6 +21,7 @@ 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; @@ -154,12 +155,11 @@ void assertNotExistTableFromRoutineBody() { @Test void assertExtractTablesFromUpdateWithTableVariableTargetExcludesVariableTable() { DatabaseType sqlServerDatabaseType = TypedSPILoader.getService(DatabaseType.class, "SQLServer"); - SimpleTableSegment targetTable = new SimpleTableSegment(new TableNameSegment(0, 0, new IdentifierValue("@MyTableVar"))); 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(targetTable) + .table(new SimpleTableSegment(new TableNameSegment(0, 0, new IdentifierValue("@MyTableVar")))) .from(fromTable) .setAssignment(new SetAssignmentSegment(0, 0, Collections.emptyList())) .build(); @@ -170,6 +170,24 @@ void assertExtractTablesFromUpdateWithTableVariableTargetExcludesVariableTable() 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)); From e02f800d63e58abb2e283eced400d81e91b5aa66 Mon Sep 17 00:00:00 2001 From: Claire Date: Sat, 25 Jul 2026 19:52:03 +0800 Subject: [PATCH 4/6] update --- .gitignore | 1 - RELEASE-NOTES.md | 2 +- .../from/type/SimpleTableSegmentBinder.java | 15 ++++ .../type/SimpleTableSegmentBinderTest.java | 73 ++++++++++++++++++- 4 files changed, 86 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index db59adc1bc8b5..95b71ee83d519 100644 --- a/.gitignore +++ b/.gitignore @@ -76,4 +76,3 @@ document/public/ .codex/.personality_migration .antlr_last_commit -.cursor/ diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 14ad3ed83b5fe..c96422fc79193 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -77,7 +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. Encrypt: Support SqlServer update statement for Specifying a table variable as the target object when use encrypt feature - [#39093](https://github.com/apache/shardingsphere/pull/39093) +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/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 da2e2b38e266e..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 @@ -331,6 +331,9 @@ private static void checkTableExists(final SQLStatementBinderContext binderConte } private static boolean isVariableTable(final SQLStatementBinderContext binderContext, final SimpleTableSegment segment) { + if (!(binderContext.getSqlStatement() instanceof UpdateStatement)) { + return false; + } if (segment.getOwner().isPresent()) { return false; } @@ -342,6 +345,13 @@ private static boolean isVariableTable(final SQLStatementBinderContext binderCon 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); } @@ -394,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; 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 151353738959a..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 @@ -31,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; @@ -83,14 +86,57 @@ void assertBindWithDBLinkContainsDBLink() { @Test void assertBindWithTableVariableContainsTableVariable() { - SimpleTableSegment simpleTableSegment = new SimpleTableSegment(new TableNameSegment(0, 10, new IdentifierValue("@MyTableVar"))); + 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(simpleTableSegment, - new SQLStatementBinderContext(createMetaData(), "foo_db", new HintValueContext(), SelectStatement.builder().databaseType(sqlServerDatabaseType).build()), tableBinderContexts); + 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))); @@ -186,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"); From 30078aa5b6fffef6aac9b47e4fdc2ca6a094de49 Mon Sep 17 00:00:00 2001 From: Claire Date: Sun, 26 Jul 2026 13:33:24 +0800 Subject: [PATCH 5/6] update release notes --- RELEASE-NOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index c96422fc79193..87cf5c17ac4f6 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -77,7 +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. SQL Binder: upport 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) From 1020724e9c301b022fcb6031b9f30e4704bf1833 Mon Sep 17 00:00:00 2001 From: Claire Date: Sun, 26 Jul 2026 16:17:41 +0800 Subject: [PATCH 6/6] update --- RELEASE-NOTES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 87cf5c17ac4f6..c96422fc79193 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -77,7 +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: upport internal binding and extraction for SQL Server UPDATE table variable targets - [#39093](https://github.com/apache/shardingsphere/pull/39093) +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)