diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java index 033f9183d..08715557a 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java @@ -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; @@ -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. * @@ -1159,4 +1163,45 @@ public Void visit(OracleNullStatement statement, S context) { public Void visit(CreateAccessMethod statement, S context) { return schemaOnly(); } + + @Override + public Void visit(AlterCollation statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(AlterDatabase statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(AlterSchema statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(AlterStatistics statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(AlterTablespaceMove statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(AlterTextSearchConfiguration statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(CreateTextSearchConfiguration statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(CreateUser statement, S context) { + return schemaOnly(); + } + } diff --git a/src/test/java/net/sf/jsqlparser/statement/StatementFeatureVisitorMetadataDdlTest.java b/src/test/java/net/sf/jsqlparser/statement/StatementFeatureVisitorMetadataDdlTest.java new file mode 100644 index 000000000..b15eb6580 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/StatementFeatureVisitorMetadataDdlTest.java @@ -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 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 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 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 accept(StatementVisitor 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()); + } +}