diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java index 3a441d28e..447c6aba2 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java @@ -28,7 +28,11 @@ 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.AlterForeignDataWrapper; +import net.sf.jsqlparser.statement.alter.AlterPolicy; import net.sf.jsqlparser.statement.alter.AlterPublication; +import net.sf.jsqlparser.statement.alter.AlterRelation; +import net.sf.jsqlparser.statement.alter.AlterServer; import net.sf.jsqlparser.statement.alter.AlterSession; import net.sf.jsqlparser.statement.alter.AlterStatistics; import net.sf.jsqlparser.statement.alter.AlterSubscription; @@ -36,6 +40,7 @@ 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.AlterUserMapping; import net.sf.jsqlparser.statement.alter.RenameTableStatement; import net.sf.jsqlparser.statement.alter.database.AlterDatabase; import net.sf.jsqlparser.statement.alter.schema.AlterSchema; @@ -43,16 +48,21 @@ 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.collation.CreateCollation; 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.fdw.CreateForeignDataWrapper; 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.rule.CreateRule; import net.sf.jsqlparser.statement.create.schema.CreateSchema; import net.sf.jsqlparser.statement.create.sequence.CreateSequence; +import net.sf.jsqlparser.statement.create.server.CreateServer; +import net.sf.jsqlparser.statement.create.statistics.CreateStatistics; import net.sf.jsqlparser.statement.create.subscription.CreateSubscription; import net.sf.jsqlparser.statement.create.subscription.SubscriptionOption; import net.sf.jsqlparser.statement.create.synonym.CreateSynonym; @@ -61,10 +71,12 @@ 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.usermapping.CreateUserMapping; 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.drop.DropPolicy; import net.sf.jsqlparser.statement.execute.Execute; import net.sf.jsqlparser.statement.export.Export; import net.sf.jsqlparser.statement.export.ExportDataStatement; @@ -1215,4 +1227,67 @@ public Void visit(CreateUser statement, S context) { return schemaOnly(); } + // These statements store definitions; visiting their children must not make a stored + // rule action or policy predicate appear to execute during the DDL statement itself. + + @Override + public Void visit(AlterRelation statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(AlterPolicy statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(DropPolicy statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(CreateStatistics statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(CreateForeignDataWrapper statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(AlterForeignDataWrapper statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(CreateServer statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(AlterServer statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(CreateUserMapping statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(AlterUserMapping statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(CreateRule statement, S context) { + return schemaOnly(); + } + + @Override + public Void visit(CreateCollation statement, S context) { + return schemaOnly(); + } + } diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java index efb294f5f..91d772e67 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java @@ -9,6 +9,21 @@ */ package net.sf.jsqlparser.statement; +import net.sf.jsqlparser.statement.alter.AlterRelation; +import net.sf.jsqlparser.statement.alter.AlterPolicy; +import net.sf.jsqlparser.statement.drop.DropPolicy; +import net.sf.jsqlparser.statement.create.statistics.CreateStatistics; +import net.sf.jsqlparser.statement.create.fdw.CreateForeignDataWrapper; +import net.sf.jsqlparser.statement.alter.AlterForeignDataWrapper; +import net.sf.jsqlparser.statement.create.server.CreateServer; +import net.sf.jsqlparser.statement.alter.AlterServer; +import net.sf.jsqlparser.statement.create.usermapping.CreateUserMapping; +import net.sf.jsqlparser.statement.alter.AlterUserMapping; +import net.sf.jsqlparser.statement.create.rule.CreateRule; +import net.sf.jsqlparser.statement.create.collation.CreateCollation; +import net.sf.jsqlparser.statement.notify.NotifyStatement; +import net.sf.jsqlparser.statement.foreign.ForeignDataStatement; + import net.sf.jsqlparser.statement.create.accessmethod.CreateAccessMethod; import net.sf.jsqlparser.statement.oracle.OracleBlock; @@ -305,7 +320,7 @@ public T visit(Insert insert, S context) { selectVisitor.visitOutputClause(insert.getOutputClause(), context); if (insert.getSelect() != null) { - insert.getSelect().accept(selectVisitor, null); + insert.getSelect().accept(selectVisitor, context); } expressionVisitor.visitUpdateSets(insert.getSetUpdateSets(), context); @@ -904,4 +919,95 @@ public T visit(OracleAssignment assignment, S context) { public T visit(CreateAccessMethod statement, S context) { return null; } + + @Override + public T visit(AlterRelation statement, S context) { + if (statement.getObjectType() != AlterRelation.ObjectType.INDEX) { + fromItemVisitor.visitFromItem(statement.getRelation(), context); + } + statement.getActions().forEach(action -> { + action.visitExpressions(expression -> expression.accept(expressionVisitor, context)); + action.visitTables(table -> table.accept(fromItemVisitor, context)); + }); + return null; + } + + @Override + public T visit(AlterPolicy statement, S context) { + fromItemVisitor.visitFromItem(statement.getTable(), context); + if (statement.getNewName() == null) { + statement.getOptions() + .visitExpressions(expression -> expression.accept(expressionVisitor, context)); + } + return null; + } + + @Override + public T visit(DropPolicy statement, S context) { + fromItemVisitor.visitFromItem(statement.getTable(), context); + return null; + } + + @Override + public T visit(CreateStatistics statement, S context) { + fromItemVisitor.visitFromItem(statement.getTable(), context); + expressionVisitor.visitExpression(statement.getExpressions(), context); + return null; + } + + private T visitForeignDataStatement(ForeignDataStatement statement, S context) { + statement.visitExpressions(expression -> expression.accept(expressionVisitor, context)); + return null; + } + + @Override + public T visit(CreateForeignDataWrapper statement, S context) { + return visitForeignDataStatement(statement, context); + } + + @Override + public T visit(AlterForeignDataWrapper statement, S context) { + return visitForeignDataStatement(statement, context); + } + + @Override + public T visit(CreateServer statement, S context) { + return visitForeignDataStatement(statement, context); + } + + @Override + public T visit(AlterServer statement, S context) { + return visitForeignDataStatement(statement, context); + } + + @Override + public T visit(CreateUserMapping statement, S context) { + return visitForeignDataStatement(statement, context); + } + + @Override + public T visit(AlterUserMapping statement, S context) { + return visitForeignDataStatement(statement, context); + } + + @Override + public T visit(CreateRule statement, S context) { + statement.visitTables(table -> table.accept(fromItemVisitor, context)); + statement.visitExpressions(expression -> expression.accept(expressionVisitor, context)); + statement.getActions().forEach(action -> action.accept(this, context)); + return null; + } + + @Override + public T visit(NotifyStatement statement, S context) { + statement.visitExpressions(expression -> expression.accept(expressionVisitor, context)); + return null; + } + + @Override + public T visit(CreateCollation statement, S context) { + statement.visitExpressions(expression -> expression.accept(expressionVisitor, context)); + return null; + } + } diff --git a/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitorAdapter.java index 50d209635..da37acddd 100644 --- a/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/statement/select/SelectVisitorAdapter.java @@ -333,7 +333,10 @@ public T visit(WithItem withItem, S context) { @Override public T visit(Values values, S context) { + visitWithItems(values.getWithItemsList(), context); expressionVisitor.visitExpressions(values.getExpressions(), context); + expressionVisitor.visitOrderBy(values.getOrderByElements(), context); + visitLimitOffsetFetch(values, context); return null; } diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java index ea09e464d..3912bac14 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java @@ -9,6 +9,11 @@ */ package net.sf.jsqlparser.util.validation.validator; +import net.sf.jsqlparser.statement.alter.AlterRelation; +import net.sf.jsqlparser.statement.alter.AlterPolicy; +import net.sf.jsqlparser.statement.drop.DropPolicy; +import net.sf.jsqlparser.statement.create.statistics.CreateStatistics; + import net.sf.jsqlparser.statement.create.accessmethod.CreateAccessMethod; import net.sf.jsqlparser.statement.create.fdw.CreateForeignDataWrapper; @@ -1168,4 +1173,42 @@ public Void visit(AlterCollation statement, S context) { validateFeature(Feature.alterCollation); return null; } + + @Override + public Void visit(AlterRelation statement, S context) { + boolean index = statement.getObjectType() == AlterRelation.ObjectType.INDEX; + validateFeature(index ? Feature.alterIndex : Feature.alterView); + if (!index) { + validateOptionalFromItem(statement.getRelation()); + } + statement.getActions().forEach(action -> { + action.visitExpressions(this::validateOptionalExpression); + action.visitTables(this::validateOptionalFromItem); + }); + return null; + } + + @Override + public Void visit(AlterPolicy statement, S context) { + validateOptionalFromItem(statement.getTable()); + if (statement.getNewName() == null) { + statement.getOptions().visitExpressions(this::validateOptionalExpression); + } + return null; + } + + @Override + public Void visit(DropPolicy statement, S context) { + validateFeature(Feature.drop); + validateOptionalFromItem(statement.getTable()); + return null; + } + + @Override + public Void visit(CreateStatistics statement, S context) { + validateOptionalFromItem(statement.getTable()); + validateOptionalExpression(statement.getExpressions()); + return null; + } + } diff --git a/src/test/java/net/sf/jsqlparser/statement/DdlVisitorTraversalTest.java b/src/test/java/net/sf/jsqlparser/statement/DdlVisitorTraversalTest.java new file mode 100644 index 000000000..0efd5168b --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/DdlVisitorTraversalTest.java @@ -0,0 +1,156 @@ +/*- + * #%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.ArrayList; +import java.util.List; +import java.util.stream.Stream; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.select.FromItemVisitorAdapter; +import net.sf.jsqlparser.statement.select.SelectVisitorAdapter; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.Validation; +import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed; +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 DdlVisitorTraversalTest { + static Stream definitions() { + return Stream.of( + Arguments.of("ALTER VIEW v ALTER COLUMN id SET DEFAULT 7", List.of("7"), + List.of("v")), + Arguments.of("ALTER INDEX ix SET (fillfactor = 70)", List.of("70"), List.of()), + Arguments.of("ALTER POLICY p ON t USING (id > 7) WITH CHECK (id < 9)", + List.of("7", "9"), List.of("t")), + Arguments.of("DROP POLICY p ON t", List.of(), List.of("t")), + Arguments.of("CREATE STATISTICS s ON (id + 7), label FROM t", List.of("7"), + List.of("t")), + Arguments.of("CREATE FOREIGN DATA WRAPPER w OPTIONS (host 'before')", + List.of("before"), List.of()), + Arguments.of("ALTER FOREIGN DATA WRAPPER w OPTIONS (SET host 'before')", + List.of("before"), List.of()), + Arguments.of( + "CREATE SERVER s TYPE 'pg' VERSION '18' FOREIGN DATA WRAPPER w OPTIONS (host 'before')", + List.of("before", "pg", "18"), List.of()), + Arguments.of("ALTER SERVER s VERSION '18' OPTIONS (SET host 'before')", + List.of("before", "18"), List.of()), + Arguments.of( + "CREATE USER MAPPING FOR CURRENT_USER SERVER s OPTIONS (user 'before')", + List.of("before"), List.of()), + Arguments.of( + "ALTER USER MAPPING FOR CURRENT_USER SERVER s OPTIONS (SET user 'before')", + List.of("before"), List.of()), + Arguments.of("CREATE COLLATION c (locale = 'C')", List.of("C"), List.of()), + Arguments.of( + "CREATE RULE r AS ON INSERT TO t WHERE NEW.id > 7 DO (INSERT INTO log VALUES (9); NOTIFY ch, 'before')", + List.of("7", "9", "before"), List.of("t", "log"))); + } + + @Test + void valuesChildrenIncludingWithAndTailKeepTheVisitorContext() throws Exception { + Object context = new Object(); + List values = new ArrayList<>(); + ExpressionVisitorAdapter expressions = new ExpressionVisitorAdapter() { + @Override + public Void visit(LongValue value, S actualContext) { + assertSame(context, actualContext); + values.add(value.getValue()); + return null; + } + }; + Statement statement = CCJSqlParserUtil.parse( + "WITH c AS (SELECT 11) VALUES (22) ORDER BY (33 + 0) LIMIT 44 OFFSET 55"); + statement.accept(new StatementVisitorAdapter<>(new SelectVisitorAdapter<>(expressions)), + context); + assertEquals(List.of(11L, 22L, 33L, 0L, 44L, 55L), values); + } + + @ParameterizedTest + @MethodSource("definitions") + void visitsAndMutatesChildrenWithContext(String sql, List expectedValues, + List expectedTables) throws Exception { + Statement statement = CCJSqlParserUtil.parse(sql); + List values = new ArrayList<>(); + List tables = new ArrayList<>(); + Object context = new Object(); + ExpressionVisitorAdapter expressions = new ExpressionVisitorAdapter() { + @Override + public Void visit(LongValue value, S actualContext) { + assertSame(context, actualContext); + values.add(value.getStringValue()); + value.setValue(value.getValue() + 1); + return null; + } + + @Override + public Void visit(StringValue value, S actualContext) { + assertSame(context, actualContext); + values.add(value.getValue()); + value.setValue("changed"); + return null; + } + }; + FromItemVisitorAdapter fromItems = new FromItemVisitorAdapter() { + @Override + public Void visit(Table table, S actualContext) { + assertSame(context, actualContext); + tables.add(table.getName()); + table.setName("changed_" + table.getName()); + return null; + } + }; + SelectVisitorAdapter selects = + new SelectVisitorAdapter<>(expressions, fromItems); + statement.accept(new StatementVisitorAdapter<>(selects), context); + assertEquals(expectedValues, values); + assertEquals(expectedTables, tables); + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(output)); + assertEquals(statement.toString(), output.toString()); + assertEquals(output.toString(), CCJSqlParserUtil.parse(output.toString()).toString()); + } + + @ParameterizedTest + @ValueSource(strings = { + "ALTER VIEW v ALTER COLUMN id SET DEFAULT abs(7)", + "ALTER POLICY p ON t USING (abs(id) > 7)", + "CREATE STATISTICS s ON (abs(id)) FROM t"}) + void validatesNestedExpressions(String sql) { + FeaturesAllowed withoutFunctions = new FeaturesAllowed().add(FeaturesAllowed.DDL); + assertFalse(new Validation(List.of(withoutFunctions), sql).validate().isEmpty()); + FeaturesAllowed withFunctions = + new FeaturesAllowed().add(FeaturesAllowed.DDL).add(Feature.function); + assertTrue(new Validation(List.of(withFunctions), sql).validate().isEmpty()); + } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE RULE r AS ON INSERT TO t DO INSERT INTO log VALUES (7)", + "CREATE RULE r AS ON SELECT TO v DO INSTEAD SELECT * FROM t", + "ALTER POLICY p ON t USING (id > 7)", + "CREATE STATISTICS s ON (id + 7), label FROM t"}) + void storedDefinitionsAreSchemaChangesRatherThanExecutedBodies(String sql) throws Exception { + StatementFeatures features = CCJSqlParserUtil.parse(sql).getFeatures(); + assertTrue(features.is(StmtFeature.MODIFIES_SCHEMA)); + assertFalse(features.is(StmtFeature.MODIFIES_DATA)); + assertFalse(features.is(StmtFeature.RETURNS_RESULT_SET)); + assertFalse(features.is(StmtFeature.READS_DATA)); + } +}