Skip to content

Commit cd35c83

Browse files
committed
fix(feature): keep generic fallback references semantic
Signed-off-by: 付典 <fudianchn@gmail.com>
1 parent 6d1ec1d commit cd35c83

4 files changed

Lines changed: 215 additions & 43 deletions

File tree

‎src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java‎

Lines changed: 42 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -9,70 +9,78 @@
99
*/
1010
package net.sf.jsqlparser.statement;
1111

12-
import net.sf.jsqlparser.statement.create.accessmethod.CreateAccessMethod;
13-
14-
import net.sf.jsqlparser.statement.oracle.OracleBlock;
15-
import net.sf.jsqlparser.statement.oracle.OracleAssignment;
16-
import net.sf.jsqlparser.statement.oracle.OracleNullStatement;
17-
18-
import net.sf.jsqlparser.statement.role.CreateRole;
19-
import net.sf.jsqlparser.statement.role.AlterRole;
20-
import net.sf.jsqlparser.statement.grant.Revoke;
21-
import net.sf.jsqlparser.statement.grant.AlterDefaultPrivileges;
22-
import net.sf.jsqlparser.statement.create.trigger.CreateTrigger;
23-
import net.sf.jsqlparser.statement.create.type.CreateType;
24-
import net.sf.jsqlparser.statement.alter.AlterType;
25-
import net.sf.jsqlparser.statement.create.domain.CreateDomain;
26-
import net.sf.jsqlparser.statement.alter.AlterDomain;
27-
import net.sf.jsqlparser.statement.create.extension.CreateExtension;
28-
import net.sf.jsqlparser.statement.alter.AlterExtension;
29-
import net.sf.jsqlparser.statement.create.publication.CreatePublication;
30-
import net.sf.jsqlparser.statement.alter.AlterPublication;
31-
import net.sf.jsqlparser.statement.create.subscription.CreateSubscription;
32-
import net.sf.jsqlparser.statement.create.subscription.SubscriptionOption;
33-
import net.sf.jsqlparser.statement.alter.AlterSubscription;
34-
12+
import java.util.ArrayList;
13+
import java.util.EnumSet;
14+
import java.util.LinkedHashSet;
15+
import java.util.List;
16+
import java.util.Locale;
17+
import java.util.Set;
18+
import java.util.function.Predicate;
3519
import net.sf.jsqlparser.JSQLParserException;
3620
import net.sf.jsqlparser.expression.AnalyticExpression;
37-
import net.sf.jsqlparser.expression.TranscodingFunction;
3821
import net.sf.jsqlparser.expression.Expression;
3922
import net.sf.jsqlparser.expression.ExpressionVisitor;
4023
import net.sf.jsqlparser.expression.ExpressionVisitorAdapter;
4124
import net.sf.jsqlparser.expression.Function;
25+
import net.sf.jsqlparser.expression.TranscodingFunction;
4226
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
4327
import net.sf.jsqlparser.statement.alter.Alter;
28+
import net.sf.jsqlparser.statement.alter.AlterDomain;
29+
import net.sf.jsqlparser.statement.alter.AlterExtension;
30+
import net.sf.jsqlparser.statement.alter.AlterPublication;
4431
import net.sf.jsqlparser.statement.alter.AlterSession;
32+
import net.sf.jsqlparser.statement.alter.AlterSubscription;
4533
import net.sf.jsqlparser.statement.alter.AlterSystemStatement;
34+
import net.sf.jsqlparser.statement.alter.AlterType;
4635
import net.sf.jsqlparser.statement.alter.RenameTableStatement;
4736
import net.sf.jsqlparser.statement.alter.sequence.AlterSequence;
4837
import net.sf.jsqlparser.statement.analyze.Analyze;
4938
import net.sf.jsqlparser.statement.comment.Comment;
39+
import net.sf.jsqlparser.statement.create.accessmethod.CreateAccessMethod;
5040
import net.sf.jsqlparser.statement.create.database.CreateDatabase;
41+
import net.sf.jsqlparser.statement.create.domain.CreateDomain;
42+
import net.sf.jsqlparser.statement.create.extension.CreateExtension;
43+
import net.sf.jsqlparser.statement.create.extension.CreateExtensionRepository;
5144
import net.sf.jsqlparser.statement.create.index.CreateIndex;
45+
import net.sf.jsqlparser.statement.create.macro.CreateMacro;
5246
import net.sf.jsqlparser.statement.create.policy.CreatePolicy;
47+
import net.sf.jsqlparser.statement.create.publication.CreatePublication;
5348
import net.sf.jsqlparser.statement.create.schema.CreateSchema;
5449
import net.sf.jsqlparser.statement.create.sequence.CreateSequence;
50+
import net.sf.jsqlparser.statement.create.subscription.CreateSubscription;
51+
import net.sf.jsqlparser.statement.create.subscription.SubscriptionOption;
5552
import net.sf.jsqlparser.statement.create.synonym.CreateSynonym;
5653
import net.sf.jsqlparser.statement.create.table.CreateTable;
54+
import net.sf.jsqlparser.statement.create.trigger.CreateTrigger;
55+
import net.sf.jsqlparser.statement.create.type.CreateType;
5756
import net.sf.jsqlparser.statement.create.view.AlterView;
5857
import net.sf.jsqlparser.statement.create.view.CreateView;
5958
import net.sf.jsqlparser.statement.delete.Delete;
6059
import net.sf.jsqlparser.statement.drop.Drop;
6160
import net.sf.jsqlparser.statement.execute.Execute;
6261
import net.sf.jsqlparser.statement.export.Export;
62+
import net.sf.jsqlparser.statement.export.ExportDataStatement;
63+
import net.sf.jsqlparser.statement.grant.AlterDefaultPrivileges;
6364
import net.sf.jsqlparser.statement.grant.Grant;
65+
import net.sf.jsqlparser.statement.grant.Revoke;
6466
import net.sf.jsqlparser.statement.imprt.Import;
6567
import net.sf.jsqlparser.statement.insert.Insert;
6668
import net.sf.jsqlparser.statement.insert.InsertBulk;
69+
import net.sf.jsqlparser.statement.load.LoadDataStatement;
6770
import net.sf.jsqlparser.statement.lock.LockStatement;
6871
import net.sf.jsqlparser.statement.merge.Merge;
72+
import net.sf.jsqlparser.statement.oracle.OracleAssignment;
73+
import net.sf.jsqlparser.statement.oracle.OracleBlock;
74+
import net.sf.jsqlparser.statement.oracle.OracleNullStatement;
6975
import net.sf.jsqlparser.statement.refresh.RefreshMaterializedViewStatement;
76+
import net.sf.jsqlparser.statement.role.AlterRole;
77+
import net.sf.jsqlparser.statement.role.CreateRole;
7078
import net.sf.jsqlparser.statement.select.FromItemVisitor;
7179
import net.sf.jsqlparser.statement.select.FromItemVisitorAdapter;
80+
import net.sf.jsqlparser.statement.select.MySqlSelectIntoClause;
7281
import net.sf.jsqlparser.statement.select.PivotVisitor;
7382
import net.sf.jsqlparser.statement.select.PivotVisitorAdapter;
7483
import net.sf.jsqlparser.statement.select.PlainSelect;
75-
import net.sf.jsqlparser.statement.select.MySqlSelectIntoClause;
7684
import net.sf.jsqlparser.statement.select.Select;
7785
import net.sf.jsqlparser.statement.select.SelectItem;
7886
import net.sf.jsqlparser.statement.select.SelectItemVisitor;
@@ -88,18 +96,6 @@
8896
import net.sf.jsqlparser.statement.update.Update;
8997
import net.sf.jsqlparser.statement.upsert.Upsert;
9098

91-
import java.util.ArrayList;
92-
import java.util.EnumSet;
93-
import java.util.LinkedHashSet;
94-
import java.util.List;
95-
import java.util.Locale;
96-
import java.util.Set;
97-
import java.util.function.Predicate;
98-
import net.sf.jsqlparser.statement.create.macro.CreateMacro;
99-
import net.sf.jsqlparser.statement.create.extension.CreateExtensionRepository;
100-
import net.sf.jsqlparser.statement.export.ExportDataStatement;
101-
import net.sf.jsqlparser.statement.load.LoadDataStatement;
102-
10399
/**
104100
* Derives a {@link StatementFeatures} verdict from a statement tree.
105101
*
@@ -186,7 +182,7 @@ public static StatementFeatures analyse(Statement statement, Predicate<String> p
186182
Analysis analysis = new Analysis(pureFunctions);
187183
StatementFeatureVisitor visitor = new StatementFeatureVisitor(analysis);
188184
statement.accept(visitor, null);
189-
analysis.failLoudIfSilent(statement.getClass().getSimpleName());
185+
analysis.failLoudIfSilent();
190186
return new StatementFeatures(analysis.certain, analysis.uncertain, analysis.unresolved);
191187
}
192188

@@ -219,7 +215,7 @@ public static StatementFeatures analyse(Statements statements,
219215
Analysis analysis = new Analysis(pureFunctions);
220216
StatementFeatureVisitor visitor = new StatementFeatureVisitor(analysis);
221217
statements.accept(visitor, null);
222-
analysis.failLoudIfSilent(Statements.class.getSimpleName());
218+
analysis.failLoudIfSilent();
223219
return new StatementFeatures(analysis.certain, analysis.uncertain, analysis.unresolved);
224220
}
225221

@@ -309,10 +305,14 @@ void suppressReads(Runnable body) {
309305
}
310306

311307
void opaque(String reference) {
308+
opaque();
309+
unresolved(reference);
310+
}
311+
312+
void opaque() {
312313
certain.add(StmtFeature.OPAQUE);
313314
uncertain.addAll(EnumSet.of(StmtFeature.READS_DATA, StmtFeature.RETURNS_RESULT_SET,
314315
StmtFeature.MODIFIES_DATA, StmtFeature.MODIFIES_SCHEMA));
315-
unresolved(reference);
316316
}
317317

318318
/**
@@ -322,9 +322,9 @@ void opaque(String reference) {
322322
* hand-maintained type table; the price is that a genuinely inert statement is also
323323
* reported opaque, which is the right direction to be wrong in.
324324
*/
325-
void failLoudIfSilent(String label) {
325+
void failLoudIfSilent() {
326326
if (certain.isEmpty()) {
327-
opaque(label);
327+
opaque();
328328
}
329329
}
330330
}

‎src/main/java/net/sf/jsqlparser/statement/StatementFeatures.java‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,12 @@ public Set<StmtFeature> getUncertain() {
7373
/**
7474
* Why the analysis is uncertain: unresolved function names, dynamic SQL markers, called
7575
* procedure names. Resolve these against your own catalogue or allow-list.
76+
*
77+
* <p>
78+
* The generic fallback for an unclassified statement adds no Java type name. It can report
79+
* {@link StmtFeature#OPAQUE} with an empty reference set; an empty set does not prove safety.
80+
* Use {@link #isOpaque()} and {@link #may(StmtFeature)} to inspect the verdict. Explicit
81+
* markers remain, including the statement-type markers for stored routine declarations.
7682
*/
7783
public Set<String> getUnresolvedReferences() {
7884
return unresolved;

‎src/site/sphinx/usage.rst‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -574,7 +574,7 @@ Convenience methods wrap the common combinations:
574574
features.modifiesSchema(); // is(MODIFIES_SCHEMA)
575575
features.isOpaque(); // CALL, EXECUTE, dynamic SQL
576576
577-
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:
577+
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:
578578

579579
.. code-block:: java
580580
:caption: Safeguarding a read-only connection
@@ -587,6 +587,10 @@ When something is merely *possible*, the analysis tells you **why**, so you can
587587
// e.g. [nextval]
588588
}
589589
590+
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.
591+
592+
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.
593+
590594
If you can prove some functions side-effect free, hand in a predicate and the uncertainty collapses:
591595

592596
.. code-block:: java
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2026 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.statement;
11+
12+
import static org.assertj.core.api.Assertions.assertThat;
13+
14+
import java.util.List;
15+
import net.sf.jsqlparser.expression.Function;
16+
import net.sf.jsqlparser.expression.LongValue;
17+
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
18+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
19+
import net.sf.jsqlparser.schema.Column;
20+
import net.sf.jsqlparser.statement.create.function.CreateFunction;
21+
import net.sf.jsqlparser.statement.oracle.OracleAssignment;
22+
import net.sf.jsqlparser.statement.oracle.OracleNullStatement;
23+
import org.junit.jupiter.api.Test;
24+
import org.junit.jupiter.params.ParameterizedTest;
25+
import org.junit.jupiter.params.provider.ValueSource;
26+
27+
class StatementFeatureFallbackTest {
28+
private static final class UnclassifiedStatement implements Statement {
29+
@Override
30+
public <T, S> T accept(StatementVisitor<T> visitor, S context) {
31+
return null;
32+
}
33+
}
34+
35+
private static void assertOpaque(StatementFeatures features, String... references) {
36+
assertThat(features.getCertain()).containsExactly(StmtFeature.OPAQUE);
37+
assertThat(features.getUncertain()).containsExactlyInAnyOrder(StmtFeature.READS_DATA,
38+
StmtFeature.RETURNS_RESULT_SET, StmtFeature.MODIFIES_DATA,
39+
StmtFeature.MODIFIES_SCHEMA);
40+
assertThat(features.getUnresolvedReferences()).containsExactlyInAnyOrder(references);
41+
}
42+
43+
@Test
44+
void unclassifiedStatementHasNoCatalogueReference() {
45+
Statement statement = new UnclassifiedStatement();
46+
assertOpaque(statement.getFeatures());
47+
assertOpaque(statement.getFeatures(name -> true));
48+
assertOpaque(StatementFeatureVisitor.analyse(statement));
49+
}
50+
51+
@Test
52+
void anonymousStatementHasNoEmptyReference() {
53+
Statement statement = new Statement() {
54+
@Override
55+
public <T, S> T accept(StatementVisitor<T> visitor, S context) {
56+
return null;
57+
}
58+
};
59+
assertOpaque(statement.getFeatures());
60+
}
61+
62+
@Test
63+
void inertOracleStatementRetainsConservativeFallback() {
64+
assertOpaque(new OracleNullStatement().getFeatures());
65+
assertOpaque(new OracleAssignment(new Column("x"), new LongValue(1)).getFeatures());
66+
}
67+
68+
@Test
69+
void fallbackPreservesFunctionReferenceFoundBeforeClassification() {
70+
Function function = new Function();
71+
function.setName("Unproven_Function");
72+
Statement assignment = new OracleAssignment(new Column("x"), function);
73+
assertOpaque(assignment.getFeatures(), "unproven_function");
74+
assertOpaque(assignment.getFeatures(name -> name.equals("unproven_function")));
75+
}
76+
77+
@ParameterizedTest
78+
@ValueSource(ints = {0, 1, 2})
79+
void aggregateFallbackHasNoContainerReference(int count) {
80+
Statements script = new Statements();
81+
for (int i = 0; i < count; i++) {
82+
script.add(new UnclassifiedStatement());
83+
}
84+
assertOpaque(StatementFeatureVisitor.analyse(script));
85+
assertOpaque(StatementFeatureVisitor.analyse(script, name -> true));
86+
List<StatementFeatures> each = StatementFeatureVisitor.analyseEach(script);
87+
assertThat(each).hasSize(count);
88+
each.forEach(StatementFeatureFallbackTest::assertOpaque);
89+
}
90+
91+
@Test
92+
void perStatementAnalysisDoesNotLeakReferencesOrResultPositions() throws Exception {
93+
Statements script = new Statements();
94+
script.add(CCJSqlParserUtil.parse("CALL Do_Something(1)"));
95+
script.add(new UnclassifiedStatement());
96+
script.add(CCJSqlParserUtil.parse("SELECT 1"));
97+
List<StatementFeatures> each = StatementFeatureVisitor.analyseEach(script);
98+
assertOpaque(each.get(0), "do_something");
99+
assertOpaque(each.get(1));
100+
assertThat(each.get(2).getCertain()).containsExactly(StmtFeature.RETURNS_RESULT_SET);
101+
assertThat(each.get(2).getUncertain()).isEmpty();
102+
assertThat(each.get(2).getUnresolvedReferences()).isEmpty();
103+
StatementFeatures union = StatementFeatureVisitor.analyse(script);
104+
assertThat(union.getCertain()).containsExactlyInAnyOrder(StmtFeature.OPAQUE,
105+
StmtFeature.RETURNS_RESULT_SET);
106+
assertThat(union.getUncertain()).containsExactlyInAnyOrder(StmtFeature.READS_DATA,
107+
StmtFeature.MODIFIES_DATA, StmtFeature.MODIFIES_SCHEMA);
108+
assertThat(union.getUnresolvedReferences()).containsExactly("do_something");
109+
}
110+
111+
@ParameterizedTest
112+
@ValueSource(strings = {"SELECT 1", "CREATE TABLE t (id INTEGER)"})
113+
void classifiedStatementsDoNotFallBack(String sql) throws Exception {
114+
StatementFeatures features = CCJSqlParserUtil.parse(sql).getFeatures();
115+
assertThat(features.getCertain()).containsExactly(sql.startsWith("SELECT")
116+
? StmtFeature.RETURNS_RESULT_SET
117+
: StmtFeature.MODIFIES_SCHEMA);
118+
assertThat(features.getUncertain()).isEmpty();
119+
assertThat(features.getUnresolvedReferences()).isEmpty();
120+
}
121+
122+
@Test
123+
void procedureAndDynamicSqlMarkersRemainResolvable() throws Exception {
124+
assertOpaque(CCJSqlParserUtil.parse("CALL Do_Something(1)").getFeatures(), "do_something");
125+
assertOpaque(CCJSqlParserUtil.parse("DO 'BEGIN NULL; END'",
126+
parser -> parser.withDialect(Dialect.POSTGRESQL)).getFeatures(), "do");
127+
assertOpaque(CCJSqlParserUtil.parse("some unsupported sql",
128+
parser -> parser.withUnsupportedStatements(true)).getFeatures(), "unsupported");
129+
}
130+
131+
@Test
132+
void explainMarkerRemainsExplicit() throws Exception {
133+
StatementFeatures features = CCJSqlParserUtil.parse("EXPLAIN SELECT 1").getFeatures();
134+
assertThat(features.getCertain()).containsExactly(StmtFeature.RETURNS_RESULT_SET);
135+
assertThat(features.getUncertain()).containsExactlyInAnyOrder(StmtFeature.MODIFIES_DATA,
136+
StmtFeature.READS_DATA);
137+
assertThat(features.getUnresolvedReferences()).containsExactly("explain");
138+
}
139+
140+
@Test
141+
void selectFunctionReferenceAndAllowListRemainEffective() throws Exception {
142+
Statement statement = CCJSqlParserUtil.parse("SELECT Unproven_Function(1)");
143+
StatementFeatures features = statement.getFeatures();
144+
assertThat(features.getCertain()).containsExactly(StmtFeature.RETURNS_RESULT_SET);
145+
assertThat(features.getUncertain()).containsExactlyInAnyOrder(StmtFeature.MODIFIES_DATA,
146+
StmtFeature.MODIFIES_SCHEMA);
147+
assertThat(features.getUnresolvedReferences()).containsExactly("unproven_function");
148+
StatementFeatures pure = statement.getFeatures(name -> name.equals("unproven_function"));
149+
assertThat(pure.getCertain()).containsExactly(StmtFeature.RETURNS_RESULT_SET);
150+
assertThat(pure.getUncertain()).isEmpty();
151+
assertThat(pure.getUnresolvedReferences()).isEmpty();
152+
}
153+
154+
@Test
155+
void storedRoutineExplicitMarkerIsOutsideGenericFallback() {
156+
StatementFeatures features = new CreateFunction().getFeatures();
157+
assertThat(features.getCertain()).containsExactly(StmtFeature.MODIFIES_SCHEMA);
158+
assertThat(features.getUncertain()).containsExactlyInAnyOrder(StmtFeature.MODIFIES_DATA,
159+
StmtFeature.READS_DATA);
160+
assertThat(features.getUnresolvedReferences()).containsExactly("createfunction");
161+
}
162+
}

0 commit comments

Comments
 (0)