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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
fix(feature): classify metadata DDL schema effects
Signed-off-by: 付典 <fudianchn@gmail.com>
  • Loading branch information
fudianchn committed Oct 2, 2026
commit 335cbcd727eaaf683f759f5a3a976b7e1f07e879
119 changes: 82 additions & 37 deletions src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,70 +9,86 @@
*/
package net.sf.jsqlparser.statement;

import net.sf.jsqlparser.statement.create.accessmethod.CreateAccessMethod;

import net.sf.jsqlparser.statement.oracle.OracleBlock;
import net.sf.jsqlparser.statement.oracle.OracleAssignment;
import net.sf.jsqlparser.statement.oracle.OracleNullStatement;

import net.sf.jsqlparser.statement.role.CreateRole;
import net.sf.jsqlparser.statement.role.AlterRole;
import net.sf.jsqlparser.statement.grant.Revoke;
import net.sf.jsqlparser.statement.grant.AlterDefaultPrivileges;
import net.sf.jsqlparser.statement.create.trigger.CreateTrigger;
import net.sf.jsqlparser.statement.create.type.CreateType;
import net.sf.jsqlparser.statement.alter.AlterType;
import net.sf.jsqlparser.statement.create.domain.CreateDomain;
import net.sf.jsqlparser.statement.alter.AlterDomain;
import net.sf.jsqlparser.statement.create.extension.CreateExtension;
import net.sf.jsqlparser.statement.alter.AlterExtension;
import net.sf.jsqlparser.statement.create.publication.CreatePublication;
import net.sf.jsqlparser.statement.alter.AlterPublication;
import net.sf.jsqlparser.statement.create.subscription.CreateSubscription;
import net.sf.jsqlparser.statement.create.subscription.SubscriptionOption;
import net.sf.jsqlparser.statement.alter.AlterSubscription;

import java.util.ArrayList;
import java.util.EnumSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.function.Predicate;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.AnalyticExpression;
import net.sf.jsqlparser.expression.TranscodingFunction;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.ExpressionVisitor;
import net.sf.jsqlparser.expression.ExpressionVisitorAdapter;
import net.sf.jsqlparser.expression.Function;
import net.sf.jsqlparser.expression.TranscodingFunction;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.alter.Alter;
import net.sf.jsqlparser.statement.alter.AlterCollation;
import net.sf.jsqlparser.statement.alter.AlterDomain;
import net.sf.jsqlparser.statement.alter.AlterExtension;
import net.sf.jsqlparser.statement.alter.AlterPublication;
import net.sf.jsqlparser.statement.alter.AlterSession;
import net.sf.jsqlparser.statement.alter.AlterStatistics;
import net.sf.jsqlparser.statement.alter.AlterSubscription;
import net.sf.jsqlparser.statement.alter.AlterSystemStatement;
import net.sf.jsqlparser.statement.alter.AlterTablespaceMove;
import net.sf.jsqlparser.statement.alter.AlterTextSearchConfiguration;
import net.sf.jsqlparser.statement.alter.AlterType;
import net.sf.jsqlparser.statement.alter.RenameTableStatement;
import net.sf.jsqlparser.statement.alter.database.AlterDatabase;
import net.sf.jsqlparser.statement.alter.schema.AlterSchema;
import net.sf.jsqlparser.statement.alter.sequence.AlterSequence;
import net.sf.jsqlparser.statement.analyze.Analyze;
import net.sf.jsqlparser.statement.comment.Comment;
import net.sf.jsqlparser.statement.create.accessmethod.CreateAccessMethod;
import net.sf.jsqlparser.statement.create.database.CreateDatabase;
import net.sf.jsqlparser.statement.create.domain.CreateDomain;
import net.sf.jsqlparser.statement.create.extension.CreateExtension;
import net.sf.jsqlparser.statement.create.extension.CreateExtensionRepository;
import net.sf.jsqlparser.statement.create.index.CreateIndex;
import net.sf.jsqlparser.statement.create.macro.CreateMacro;
import net.sf.jsqlparser.statement.create.policy.CreatePolicy;
import net.sf.jsqlparser.statement.create.publication.CreatePublication;
import net.sf.jsqlparser.statement.create.schema.CreateSchema;
import net.sf.jsqlparser.statement.create.sequence.CreateSequence;
import net.sf.jsqlparser.statement.create.subscription.CreateSubscription;
import net.sf.jsqlparser.statement.create.subscription.SubscriptionOption;
import net.sf.jsqlparser.statement.create.synonym.CreateSynonym;
import net.sf.jsqlparser.statement.create.table.CreateTable;
import net.sf.jsqlparser.statement.create.textsearch.CreateTextSearchConfiguration;
import net.sf.jsqlparser.statement.create.trigger.CreateTrigger;
import net.sf.jsqlparser.statement.create.type.CreateType;
import net.sf.jsqlparser.statement.create.user.CreateUser;
import net.sf.jsqlparser.statement.create.view.AlterView;
import net.sf.jsqlparser.statement.create.view.CreateView;
import net.sf.jsqlparser.statement.delete.Delete;
import net.sf.jsqlparser.statement.drop.Drop;
import net.sf.jsqlparser.statement.execute.Execute;
import net.sf.jsqlparser.statement.export.Export;
import net.sf.jsqlparser.statement.export.ExportDataStatement;
import net.sf.jsqlparser.statement.grant.AlterDefaultPrivileges;
import net.sf.jsqlparser.statement.grant.Grant;
import net.sf.jsqlparser.statement.grant.Revoke;
import net.sf.jsqlparser.statement.imprt.Import;
import net.sf.jsqlparser.statement.insert.Insert;
import net.sf.jsqlparser.statement.insert.InsertBulk;
import net.sf.jsqlparser.statement.load.LoadDataStatement;
import net.sf.jsqlparser.statement.lock.LockStatement;
import net.sf.jsqlparser.statement.merge.Merge;
import net.sf.jsqlparser.statement.oracle.OracleAssignment;
import net.sf.jsqlparser.statement.oracle.OracleBlock;
import net.sf.jsqlparser.statement.oracle.OracleNullStatement;
import net.sf.jsqlparser.statement.refresh.RefreshMaterializedViewStatement;
import net.sf.jsqlparser.statement.role.AlterRole;
import net.sf.jsqlparser.statement.role.CreateRole;
import net.sf.jsqlparser.statement.select.FromItemVisitor;
import net.sf.jsqlparser.statement.select.FromItemVisitorAdapter;
import net.sf.jsqlparser.statement.select.MySqlSelectIntoClause;
import net.sf.jsqlparser.statement.select.PivotVisitor;
import net.sf.jsqlparser.statement.select.PivotVisitorAdapter;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.MySqlSelectIntoClause;
import net.sf.jsqlparser.statement.select.Select;
import net.sf.jsqlparser.statement.select.SelectItem;
import net.sf.jsqlparser.statement.select.SelectItemVisitor;
Expand All @@ -88,18 +104,6 @@
import net.sf.jsqlparser.statement.update.Update;
import net.sf.jsqlparser.statement.upsert.Upsert;

import java.util.ArrayList;
import java.util.EnumSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.function.Predicate;
import net.sf.jsqlparser.statement.create.macro.CreateMacro;
import net.sf.jsqlparser.statement.create.extension.CreateExtensionRepository;
import net.sf.jsqlparser.statement.export.ExportDataStatement;
import net.sf.jsqlparser.statement.load.LoadDataStatement;

/**
* Derives a {@link StatementFeatures} verdict from a statement tree.
*
Expand Down Expand Up @@ -1159,4 +1163,45 @@ public <S> Void visit(OracleNullStatement statement, S context) {
public <S> Void visit(CreateAccessMethod statement, S context) {
return schemaOnly();
}

@Override
public <S> Void visit(AlterCollation statement, S context) {
return schemaOnly();
}

@Override
public <S> Void visit(AlterDatabase statement, S context) {
return schemaOnly();
}

@Override
public <S> Void visit(AlterSchema statement, S context) {
return schemaOnly();
}

@Override
public <S> Void visit(AlterStatistics statement, S context) {
return schemaOnly();
}

@Override
public <S> Void visit(AlterTablespaceMove statement, S context) {
return schemaOnly();
}

@Override
public <S> Void visit(AlterTextSearchConfiguration statement, S context) {
return schemaOnly();
}

@Override
public <S> Void visit(CreateTextSearchConfiguration statement, S context) {
return schemaOnly();
}

@Override
public <S> Void visit(CreateUser statement, S context) {
return schemaOnly();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*-
* #%L
* JSQLParser library
* %%
* Copyright (C) 2004 - 2026 JSQLParser
* %%
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
* #L%
*/
package net.sf.jsqlparser.statement;

import static org.junit.jupiter.api.Assertions.*;

import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.alter.AlterCollation;
import net.sf.jsqlparser.statement.alter.AlterStatistics;
import net.sf.jsqlparser.statement.alter.AlterTablespaceMove;
import net.sf.jsqlparser.statement.alter.AlterTextSearchConfiguration;
import net.sf.jsqlparser.statement.alter.database.AlterDatabase;
import net.sf.jsqlparser.statement.alter.schema.AlterSchema;
import net.sf.jsqlparser.statement.create.textsearch.CreateTextSearchConfiguration;
import net.sf.jsqlparser.statement.create.user.CreateUser;
import net.sf.jsqlparser.test.TestUtils;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;

class StatementFeatureVisitorMetadataDdlTest {
static Stream<Arguments> definitions() {
return Stream.of(
Arguments.of("ALTER COLLATION c REFRESH VERSION", AlterCollation.class),
Arguments.of("ALTER COLLATION c RENAME TO c2", AlterCollation.class),
Arguments.of("ALTER DATABASE d READ ONLY = 0", AlterDatabase.class),
Arguments.of("ALTER DATABASE d CHARACTER SET utf8mb4 COLLATE utf8mb4_bin",
AlterDatabase.class),
Arguments.of("ALTER SCHEMA s RENAME TO s2", AlterSchema.class),
Arguments.of("ALTER SCHEMA s OWNER TO CURRENT_USER", AlterSchema.class),
Arguments.of("ALTER STATISTICS s RENAME TO s2", AlterStatistics.class),
Arguments.of("ALTER STATISTICS s SET STATISTICS 500", AlterStatistics.class),
Arguments.of("ALTER TABLE ALL IN TABLESPACE old_space SET TABLESPACE new_space",
AlterTablespaceMove.class),
Arguments.of(
"ALTER INDEX ALL IN TABLESPACE old_space SET TABLESPACE new_space NOWAIT",
AlterTablespaceMove.class),
Arguments.of("CREATE TEXT SEARCH CONFIGURATION c (COPY = pg_catalog.simple)",
CreateTextSearchConfiguration.class),
Arguments.of("CREATE TEXT SEARCH CONFIGURATION c (PARSER = p)",
CreateTextSearchConfiguration.class),
Arguments.of("ALTER TEXT SEARCH CONFIGURATION c ADD MAPPING FOR word WITH simple",
AlterTextSearchConfiguration.class),
Arguments.of("ALTER TEXT SEARCH CONFIGURATION c DROP MAPPING FOR word",
AlterTextSearchConfiguration.class),
Arguments.of("CREATE USER u", CreateUser.class),
Arguments.of("CREATE USER u IDENTIFIED BY 'password'", CreateUser.class));
}

@ParameterizedTest
@MethodSource("definitions")
void metadataDefinitionsHaveOnlySchemaEffectsAcrossEntryPoints(String sql, Class<?> type)
throws Exception {
Statement statement = TestUtils.assertSqlCanBeParsedAndDeparsed(sql);
assertInstanceOf(type, statement);
assertSchemaOnly(statement.getFeatures());
assertSchemaOnly(StatementFeatureVisitor.analyse(statement));
assertSchemaOnly(StatementFeatureVisitor.analyse(sql));
assertSchemaOnly(StatementFeatureVisitor.analyse(statement, name -> true));
Statements script = CCJSqlParserUtil.parseStatements(sql);
assertSchemaOnly(StatementFeatureVisitor.analyse(script));
List<StatementFeatures> each = StatementFeatureVisitor.analyseEach(script);
assertEquals(1, each.size());
assertSchemaOnly(each.get(0));
}

@ParameterizedTest
@MethodSource("definitions")
void classificationDoesNotSuppressFollowingExecutedSql(String sql, Class<?> type)
throws Exception {
Statements script = CCJSqlParserUtil.parseStatements(sql + "; SELECT f(id) FROM t");
assertInstanceOf(type, script.getStatements().get(0));
List<StatementFeatures> each = StatementFeatureVisitor.analyseEach(script);
assertSchemaOnly(each.get(0));
assertEquals(Set.of("f"), each.get(1).getUnresolvedReferences());
assertEquals(EnumSet.of(StmtFeature.READS_DATA, StmtFeature.RETURNS_RESULT_SET),
each.get(1).getCertain());
StatementFeatures union = StatementFeatureVisitor.analyse(script);
assertEquals(EnumSet.of(StmtFeature.MODIFIES_SCHEMA, StmtFeature.READS_DATA,
StmtFeature.RETURNS_RESULT_SET), union.getCertain());
assertEquals(Set.of("f"), union.getUnresolvedReferences());
assertTrue(union.mayModifyData());
}

@ParameterizedTest
@ValueSource(
strings = {"CREATE ROLE r", "CREATE TRIGGER tr AFTER INSERT ON t EXECUTE FUNCTION f()",
"CREATE POLICY p ON t USING (f(id) > 0)"})
void existingStoredDefinitionsRemainSchemaOnly(String sql) throws Exception {
assertSchemaOnly(TestUtils.assertSqlCanBeParsedAndDeparsed(sql).getFeatures());
}

@Test
void unknownAndUnsupportedStatementsRemainConservative() throws Exception {
Statement unknown = new Statement() {
@Override
public <T, S> T accept(StatementVisitor<T> visitor, S context) {
return null;
}
};
assertOpaque(unknown.getFeatures());
assertOpaque(StatementFeatureVisitor.analyse(new Statements()));
StatementFeatures unsupported = CCJSqlParserUtil
.parse("garbage", p -> p.withUnsupportedStatements(true)).getFeatures();
assertOpaque(unsupported);
assertEquals(Set.of("unsupported"), unsupported.getUnresolvedReferences());
}

private static void assertSchemaOnly(StatementFeatures features) {
assertEquals(EnumSet.of(StmtFeature.MODIFIES_SCHEMA), features.getCertain());
assertTrue(features.getUncertain().isEmpty());
assertTrue(features.getUnresolvedReferences().isEmpty());
}

private static void assertOpaque(StatementFeatures features) {
assertEquals(EnumSet.of(StmtFeature.OPAQUE), features.getCertain());
assertEquals(
EnumSet.of(StmtFeature.READS_DATA, StmtFeature.RETURNS_RESULT_SET,
StmtFeature.MODIFIES_DATA, StmtFeature.MODIFIES_SCHEMA),
features.getUncertain());
}
}
Loading