diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java index 08715557a..b3f9d505e 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java @@ -190,7 +190,7 @@ public static StatementFeatures analyse(Statement statement, Predicate p Analysis analysis = new Analysis(pureFunctions); StatementFeatureVisitor visitor = new StatementFeatureVisitor(analysis); statement.accept(visitor, null); - analysis.failLoudIfSilent(statement.getClass().getSimpleName()); + analysis.failLoudIfSilent(); return new StatementFeatures(analysis.certain, analysis.uncertain, analysis.unresolved); } @@ -223,7 +223,7 @@ public static StatementFeatures analyse(Statements statements, Analysis analysis = new Analysis(pureFunctions); StatementFeatureVisitor visitor = new StatementFeatureVisitor(analysis); statements.accept(visitor, null); - analysis.failLoudIfSilent(Statements.class.getSimpleName()); + analysis.failLoudIfSilent(); return new StatementFeatures(analysis.certain, analysis.uncertain, analysis.unresolved); } @@ -313,10 +313,14 @@ void suppressReads(Runnable body) { } void opaque(String reference) { + opaque(); + unresolved(reference); + } + + void opaque() { certain.add(StmtFeature.OPAQUE); uncertain.addAll(EnumSet.of(StmtFeature.READS_DATA, StmtFeature.RETURNS_RESULT_SET, StmtFeature.MODIFIES_DATA, StmtFeature.MODIFIES_SCHEMA)); - unresolved(reference); } /** @@ -326,9 +330,9 @@ void opaque(String reference) { * hand-maintained type table; the price is that a genuinely inert statement is also * reported opaque, which is the right direction to be wrong in. */ - void failLoudIfSilent(String label) { + void failLoudIfSilent() { if (certain.isEmpty()) { - opaque(label); + opaque(); } } } diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementFeatures.java b/src/main/java/net/sf/jsqlparser/statement/StatementFeatures.java index d92103915..bf80a1b03 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementFeatures.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementFeatures.java @@ -73,6 +73,12 @@ public Set getUncertain() { /** * Why the analysis is uncertain: unresolved function names, dynamic SQL markers, called * procedure names. Resolve these against your own catalogue or allow-list. + * + *

+ * The generic fallback for an unclassified statement adds no Java type name. It can report + * {@link StmtFeature#OPAQUE} with an empty reference set; an empty set does not prove safety. + * Use {@link #isOpaque()} and {@link #may(StmtFeature)} to inspect the verdict. Explicit + * markers remain, including the statement-type markers for stored routine declarations. */ public Set getUnresolvedReferences() { return unresolved; diff --git a/src/site/sphinx/usage.rst b/src/site/sphinx/usage.rst index a4a8a304f..dc19eb55f 100644 --- a/src/site/sphinx/usage.rst +++ b/src/site/sphinx/usage.rst @@ -574,7 +574,7 @@ Convenience methods wrap the common combinations: features.modifiesSchema(); // is(MODIFIES_SCHEMA) features.isOpaque(); // CALL, EXECUTE, dynamic SQL -When something is merely *possible*, the analysis tells you **why**, so you can resolve it against your own catalogue or allow-list rather than guessing: +When the analysis identifies an unresolved function, procedure or dynamic SQL marker, it records the reference so you can consult your own catalogue or allow-list: .. code-block:: java :caption: Safeguarding a read-only connection @@ -587,6 +587,10 @@ When something is merely *possible*, the analysis tells you **why**, so you can // e.g. [nextval] } +The generic fallback for an unclassified statement preserves ``OPAQUE`` and the possible effects ``READS_DATA``, ``RETURNS_RESULT_SET``, ``MODIFIES_DATA`` and ``MODIFIES_SCHEMA``, but does not add a Java statement or container class name to ``getUnresolvedReferences()``. The reference set can therefore be empty while the effects remain unknown. Check ``isOpaque()`` and ``may(..)``; an empty reference set alone does not establish safety. + +Existing explicit references and markers remain, including procedure and function names, ``do``, ``unsupported`` and ``explain``. Stored routine declarations also retain their existing statement-type markers, such as ``createfunction``; these are independent of the generic fallback. + If you can prove some functions side-effect free, hand in a predicate and the uncertainty collapses: .. code-block:: java diff --git a/src/test/java/net/sf/jsqlparser/statement/StatementFeatureFallbackTest.java b/src/test/java/net/sf/jsqlparser/statement/StatementFeatureFallbackTest.java new file mode 100644 index 000000000..7826cddd8 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/StatementFeatureFallbackTest.java @@ -0,0 +1,162 @@ +/*- + * #%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.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import net.sf.jsqlparser.expression.Function; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.create.function.CreateFunction; +import net.sf.jsqlparser.statement.oracle.OracleAssignment; +import net.sf.jsqlparser.statement.oracle.OracleNullStatement; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class StatementFeatureFallbackTest { + private static final class UnclassifiedStatement implements Statement { + @Override + public T accept(StatementVisitor visitor, S context) { + return null; + } + } + + private static void assertOpaque(StatementFeatures features, String... references) { + assertThat(features.getCertain()).containsExactly(StmtFeature.OPAQUE); + assertThat(features.getUncertain()).containsExactlyInAnyOrder(StmtFeature.READS_DATA, + StmtFeature.RETURNS_RESULT_SET, StmtFeature.MODIFIES_DATA, + StmtFeature.MODIFIES_SCHEMA); + assertThat(features.getUnresolvedReferences()).containsExactlyInAnyOrder(references); + } + + @Test + void unclassifiedStatementHasNoCatalogueReference() { + Statement statement = new UnclassifiedStatement(); + assertOpaque(statement.getFeatures()); + assertOpaque(statement.getFeatures(name -> true)); + assertOpaque(StatementFeatureVisitor.analyse(statement)); + } + + @Test + void anonymousStatementHasNoEmptyReference() { + Statement statement = new Statement() { + @Override + public T accept(StatementVisitor visitor, S context) { + return null; + } + }; + assertOpaque(statement.getFeatures()); + } + + @Test + void inertOracleStatementRetainsConservativeFallback() { + assertOpaque(new OracleNullStatement().getFeatures()); + assertOpaque(new OracleAssignment(new Column("x"), new LongValue(1)).getFeatures()); + } + + @Test + void fallbackPreservesFunctionReferenceFoundBeforeClassification() { + Function function = new Function(); + function.setName("Unproven_Function"); + Statement assignment = new OracleAssignment(new Column("x"), function); + assertOpaque(assignment.getFeatures(), "unproven_function"); + assertOpaque(assignment.getFeatures(name -> name.equals("unproven_function"))); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2}) + void aggregateFallbackHasNoContainerReference(int count) { + Statements script = new Statements(); + for (int i = 0; i < count; i++) { + script.add(new UnclassifiedStatement()); + } + assertOpaque(StatementFeatureVisitor.analyse(script)); + assertOpaque(StatementFeatureVisitor.analyse(script, name -> true)); + List each = StatementFeatureVisitor.analyseEach(script); + assertThat(each).hasSize(count); + each.forEach(StatementFeatureFallbackTest::assertOpaque); + } + + @Test + void perStatementAnalysisDoesNotLeakReferencesOrResultPositions() throws Exception { + Statements script = new Statements(); + script.add(CCJSqlParserUtil.parse("CALL Do_Something(1)")); + script.add(new UnclassifiedStatement()); + script.add(CCJSqlParserUtil.parse("SELECT 1")); + List each = StatementFeatureVisitor.analyseEach(script); + assertOpaque(each.get(0), "do_something"); + assertOpaque(each.get(1)); + assertThat(each.get(2).getCertain()).containsExactly(StmtFeature.RETURNS_RESULT_SET); + assertThat(each.get(2).getUncertain()).isEmpty(); + assertThat(each.get(2).getUnresolvedReferences()).isEmpty(); + StatementFeatures union = StatementFeatureVisitor.analyse(script); + assertThat(union.getCertain()).containsExactlyInAnyOrder(StmtFeature.OPAQUE, + StmtFeature.RETURNS_RESULT_SET); + assertThat(union.getUncertain()).containsExactlyInAnyOrder(StmtFeature.READS_DATA, + StmtFeature.MODIFIES_DATA, StmtFeature.MODIFIES_SCHEMA); + assertThat(union.getUnresolvedReferences()).containsExactly("do_something"); + } + + @ParameterizedTest + @ValueSource(strings = {"SELECT 1", "CREATE TABLE t (id INTEGER)"}) + void classifiedStatementsDoNotFallBack(String sql) throws Exception { + StatementFeatures features = CCJSqlParserUtil.parse(sql).getFeatures(); + assertThat(features.getCertain()).containsExactly(sql.startsWith("SELECT") + ? StmtFeature.RETURNS_RESULT_SET + : StmtFeature.MODIFIES_SCHEMA); + assertThat(features.getUncertain()).isEmpty(); + assertThat(features.getUnresolvedReferences()).isEmpty(); + } + + @Test + void procedureAndDynamicSqlMarkersRemainResolvable() throws Exception { + assertOpaque(CCJSqlParserUtil.parse("CALL Do_Something(1)").getFeatures(), "do_something"); + assertOpaque(CCJSqlParserUtil.parse("DO 'BEGIN NULL; END'", + parser -> parser.withDialect(Dialect.POSTGRESQL)).getFeatures(), "do"); + assertOpaque(CCJSqlParserUtil.parse("some unsupported sql", + parser -> parser.withUnsupportedStatements(true)).getFeatures(), "unsupported"); + } + + @Test + void explainMarkerRemainsExplicit() throws Exception { + StatementFeatures features = CCJSqlParserUtil.parse("EXPLAIN SELECT 1").getFeatures(); + assertThat(features.getCertain()).containsExactly(StmtFeature.RETURNS_RESULT_SET); + assertThat(features.getUncertain()).containsExactlyInAnyOrder(StmtFeature.MODIFIES_DATA, + StmtFeature.READS_DATA); + assertThat(features.getUnresolvedReferences()).containsExactly("explain"); + } + + @Test + void selectFunctionReferenceAndAllowListRemainEffective() throws Exception { + Statement statement = CCJSqlParserUtil.parse("SELECT Unproven_Function(1)"); + StatementFeatures features = statement.getFeatures(); + assertThat(features.getCertain()).containsExactly(StmtFeature.RETURNS_RESULT_SET); + assertThat(features.getUncertain()).containsExactlyInAnyOrder(StmtFeature.MODIFIES_DATA, + StmtFeature.MODIFIES_SCHEMA); + assertThat(features.getUnresolvedReferences()).containsExactly("unproven_function"); + StatementFeatures pure = statement.getFeatures(name -> name.equals("unproven_function")); + assertThat(pure.getCertain()).containsExactly(StmtFeature.RETURNS_RESULT_SET); + assertThat(pure.getUncertain()).isEmpty(); + assertThat(pure.getUnresolvedReferences()).isEmpty(); + } + + @Test + void storedRoutineExplicitMarkerIsOutsideGenericFallback() { + StatementFeatures features = new CreateFunction().getFeatures(); + assertThat(features.getCertain()).containsExactly(StmtFeature.MODIFIES_SCHEMA); + assertThat(features.getUncertain()).containsExactlyInAnyOrder(StmtFeature.MODIFIES_DATA, + StmtFeature.READS_DATA); + assertThat(features.getUnresolvedReferences()).containsExactly("createfunction"); + } +}