Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ public static StatementFeatures analyse(Statement statement, Predicate<String> 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);
}

Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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);
}

/**
Expand All @@ -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();
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ public Set<StmtFeature> getUncertain() {
/**
* Why the analysis is uncertain: unresolved function names, dynamic SQL markers, called
* procedure names. Resolve these against your own catalogue or allow-list.
*
* <p>
* 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<String> getUnresolvedReferences() {
return unresolved;
Expand Down
6 changes: 5 additions & 1 deletion src/site/sphinx/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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, S> T accept(StatementVisitor<T> 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, S> T accept(StatementVisitor<T> 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<StatementFeatures> 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<StatementFeatures> 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");
}
}
Loading