Skip to content

Commit 22023a4

Browse files
feat(statement): add StmtFeature classification via Statement#getFeatures()
Signed-off-by: Andreas Reichel <andreas@manticore-projects.com>
1 parent 1b57bfa commit 22023a4

8 files changed

Lines changed: 1544 additions & 47 deletions

File tree

README.md

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ EqualsTo equalsTo = (EqualsTo) select.getWhere();
6262
Column a = (Column) equalsTo.getLeftExpression();
6363
Column b = (Column) equalsTo.getRightExpression();
6464
Assertions.assertEquals("a", a.getColumnName());
65-
Assertions.assertEquals("b", b.getColumnName());
65+
Assertions.assertEquals("b", b.getColumnName());
6666
```
6767

6868
The tree is traversable with the Visitor pattern, and the same object model works in reverse:
@@ -148,6 +148,40 @@ Beyond statement shapes, the grammar handles nested sub-selects, bind parameters
148148
array-literal ambiguity. The complete reference is on the
149149
[syntax page](https://jsqlparser.github.io/JSqlParser/syntax.html).
150150

151+
## Statement classification
152+
153+
Any parsed statement can say what it actually does — no second parse, no visitor to write:
154+
155+
```java
156+
StatementFeatures features = CCJSqlParserUtil.parse(sqlStr).getFeatures();
157+
158+
// safeguard a read-only client before anything reaches the database
159+
if (connection.isReadOnly() && features.mayModifyData()) {
160+
throw new SQLException("rejected: " + features.getUnresolvedReferences());
161+
}
162+
163+
// dispatch correctly
164+
if (features.returnsResultSet()) { statement.executeQuery(sqlStr); }
165+
else { statement.executeUpdate(sqlStr); }
166+
```
167+
168+
This is not `sqlStr.startsWith("SELECT")` with extra steps. `RETURNING` turns DML into a row
169+
source, a data-modifying CTE hides a `DELETE` inside a `SELECT`, and `INSERT INTO x SELECT ..`
170+
contains a query but returns nothing:
171+
172+
| SQL | returns rows | modifies data |
173+
|---|---|---|
174+
| `SELECT * FROM t` | yes | no |
175+
| `INSERT INTO x SELECT * FROM t` | no | yes |
176+
| `DELETE FROM t RETURNING *` | yes | yes |
177+
| `WITH c AS (DELETE FROM t RETURNING *) SELECT * FROM c` | yes | yes |
178+
| `SELECT nextval('s')` | yes | *unproven* |
179+
180+
Features are not mutually exclusive, and each is three-valued: proven, not excludable, or ruled
181+
out. `is()` answers "did the grammar prove it", `may()` answers "could it be ruled out" — so a
182+
guard uses `may()` and a dispatcher uses `is()`. Function volatility is not a syntactic property,
183+
so anything the caller has not declared pure stays unproven and is listed by name.
184+
151185
## Piped SQL
152186

153187
Support is progressing for Piped SQL, which writes queries in the order they actually

src/main/java/net/sf/jsqlparser/statement/Statement.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,26 @@
1111

1212
import net.sf.jsqlparser.Model;
1313

14+
import java.util.function.Predicate;
15+
1416
public interface Statement extends Model {
1517
<T, S> T accept(StatementVisitor<T> statementVisitor, S context);
1618

1719
default void accept(StatementVisitor<?> statementVisitor) {
1820
accept(statementVisitor, null);
1921
}
22+
23+
/**
24+
* Static feature analysis of this statement tree. Not cached: the AST is mutable and publicly
25+
* constructible, and a stale bitmask is worse than a recomputation (micro seconds against a
26+
* millisecond-scale parse).
27+
*/
28+
default StatementFeatures getFeatures() {
29+
return StatementFeatureVisitor.analyse(this);
30+
}
31+
32+
/** Same, with a caller-supplied allow-list of provably side-effect-free functions. */
33+
default StatementFeatures getFeatures(Predicate<String> pureFunctions) {
34+
return StatementFeatureVisitor.analyse(this, pureFunctions);
35+
}
2036
}

0 commit comments

Comments
 (0)