Skip to content

Commit 626548d

Browse files
committed
perf: make lookahead cleanup repeatable
Report fresh base/working-tree JavaCC contexts, transitive dependencies, nullable/FOLLOW effects and stale evidence through a Gradle entry and CI artifact. Keep unknown Java effects conservative and audit code outside the library JAR. Remove the redundant DBMSDestination dispatch probe and cover table/statement destinations and required cloud disambiguation. Missing TABLE names now fail after TABLE instead of falling through to the STATEMENT alternative. Signed-off-by: 付典 <fudianchn@gmail.com>
1 parent 68c035b commit 626548d

10 files changed

Lines changed: 1287 additions & 2 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,20 @@ jobs:
4545
- name: Check for grammar ambiguities
4646
run: ./gradlew checkGrammarAmbiguity --no-daemon
4747

48+
- name: Report changed lookahead contexts
49+
env:
50+
LOOKAHEAD_BASE: ${{ github.event.pull_request.base.sha || github.event.before || github.sha }}
51+
run: ./gradlew lookaheadAudit --no-daemon -PlookaheadBase="$LOOKAHEAD_BASE"
52+
53+
- name: Upload lookahead review report
54+
if: always()
55+
uses: actions/upload-artifact@v5
56+
with:
57+
name: lookahead-review
58+
path: build/reports/lookahead/
59+
retention-days: 7
60+
if-no-files-found: ignore
61+
4862
- name: Upload grammar warning report
4963
if: failure()
5064
uses: actions/upload-artifact@v5
@@ -216,4 +230,4 @@ jobs:
216230

217231
- name: Deploy to GitHub Pages
218232
id: deployment
219-
uses: actions/deploy-pages@v5
233+
uses: actions/deploy-pages@v5

‎build.gradle‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -846,3 +846,23 @@ tasks.register('checkGrammarAmbiguity') {
846846
tasks.named('check') {
847847
dependsOn('checkGrammarAmbiguity')
848848
}
849+
850+
// Report base/working-tree context changes without making historical lookahead debt a new gate.
851+
// The analyzer lives in test sources: no audit classes or dependencies enter the library JAR.
852+
tasks.register('lookaheadAudit', JavaExec) {
853+
group = 'verification'
854+
description = 'Reports changed lookahead decisions and stale evidence against a Git base.'
855+
dependsOn(testClasses)
856+
classpath = sourceSets.test.runtimeClasspath
857+
mainClass = 'net.sf.jsqlparser.parser.audit.LookaheadAudit'
858+
javaLauncher = javaToolchains.launcherFor(java.toolchain)
859+
jvmArgs '-Xss4m', '-Xmx1g', '-Dfile.encoding=UTF-8'
860+
workingDir = projectDir
861+
doFirst {
862+
args = [projectDir.absolutePath,
863+
providers.gradleProperty('lookaheadBase').getOrElse('HEAD'),
864+
layout.buildDirectory.dir('reports/lookahead').get().asFile.absolutePath,
865+
file('config/lookahead-evidence.tsv').absolutePath,
866+
providers.gradleProperty('lookaheadFailOnFindings').getOrElse('false')]
867+
}
868+
}

‎config/lookahead-evidence.tsv‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# key<TAB>context fingerprint<TAB>reason<TAB>regression test
2+
ConnectionOrCloudConnectionDefinition 2fb828bb0d5538e6dbe372911ec28893c2a85bab60764e386f71cd2c72bd1254 AT conn and AT CLOUD share the first token; retain the second-token decision. LookaheadCleanupTest.preservesCloudAndOrdinaryConnections

‎src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4559,7 +4559,6 @@ DBMSDestination DBMSDestination() #DBMSDestination: {
45594559
connectionDefinition = ConnectionDefinition() { dbmsDestination.setConnectionDefinition(connectionDefinition); }
45604560

45614561
(
4562-
LOOKAHEAD(3)
45634562
<K_TABLE> table = Table() { dbmsDestination.setTable(table); }
45644563
[ LOOKAHEAD(2) columns = ParenthesedColumnList() { dbmsDestination.setColumns(columns); } ]
45654564
[ LOOKAHEAD(2) dbmsTableDestinationOptions = DBMSTableDestinationOptionList() { dbmsDestination.setDBMSTableDestinationOptions(dbmsTableDestinationOptions); } ]

‎src/site/sphinx/contribution.rst‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,31 @@ When adding a new keyword token to the Grammar:
120120
2) If the keyword must be reserved (e. |_| g. core SQL syntax like ``SELECT``, ``FROM``, ``WHERE``), add it to the ``Reserved SQL Keywords`` TOKEN block **after** the ``MAX_NON_RESERVED_WORD`` sentinel.
121121
3) Verify that existing tests pass and that the keyword can be used as a ``Schema``, ``Table``, ``Column``, ``Function`` or ``Alias`` name where expected.
122122

123+
Review Lookahead Changes
124+
------------------------------
125+
126+
Run the reusable lookahead report against the commit used as the review baseline:
127+
128+
.. code-block:: shell
129+
130+
./gradlew lookaheadAudit -PlookaheadBase=HEAD
131+
132+
The default baseline is ``HEAD``, so this command also examines uncommitted grammar changes. CI uses the pull request base commit. Results are written to ``build/reports/lookahead/report.json``. The analyzer runs with the project's existing JDK and JavaCC dependencies; it does not require Python or modify sources.
133+
134+
The machine calculation is fresh for both versions. Review the changed decisions, dependency roots, and stale evidence in the summary instead of manually rechecking every unchanged lookahead. Structural anchors ignore source line numbers and unrelated lookahead insertions. Identical anchors remain ambiguous and cannot reuse an individual evidence record.
135+
136+
The report computes lexical FIRST/nullable/FOLLOW facts and follows direct and indirect production calls. Nullable competing alternatives contribute FOLLOW to the decision; unknown actions on either side prevent a disjoint-prefix suggestion. A changed callee can invalidate evidence even when the local FIRST set stays the same; a caller's nullable suffix can change a callee's FOLLOW context. Semantic predicates, Java actions and production declarations are treated conservatively, rather than assumed equivalent.
137+
138+
All checked-in production Java files, runtime resources, ``build.gradle``, ``settings.gradle``, ``gradle.properties``, ``pom.xml`` and Gradle wrapper properties are monitored as opaque inputs. The dedicated cleanup regression test, analyzer sources and resolved JavaCC core/Java generator JARs are also bound to the evidence. Unrecognized watched symlinks fail the check instead of being skipped. Generated parser behavior is bound to the grammar and generator JARs; commit-derived build/version metadata is excluded. Evidence covers tracked static inputs and tested behavior, not every possible runtime configuration.
139+
140+
``config/lookahead-evidence.tsv`` stores a small number of investigated decisions, their exact context fingerprint, reason and regression reference. It is not an approval of all historical sites or permission to publish changes. The report writes current unambiguous bindings to ``binding-candidates.tsv``; review and rerun the referenced tests before updating a stale record. A production name is accepted only when it identifies one decision; otherwise use a unique structural anchor or disambiguate the case.
141+
142+
Pure one-terminal probes can suggest an equivalent token-kind condition. Non-nullable branches with disjoint FIRST sets can suggest default dispatch instead of a deeper scan. These are reusable syntactic rules, not an equivalence theorem for imperative JavaCC code. Retain state/dialect/interruption checks, verify complete consumption and AST behavior, and test malformed neighbors before adopting a candidate.
143+
144+
Error locations can change when a rejected branch is entered directly. In the ``DBMSDestination`` cleanup, ``EXA AT conn TABLE`` remains invalid, but now reports the missing table name after consuming ``TABLE`` instead of falling through to the ``STATEMENT`` alternative. The dedicated regression checks this consumption boundary; exact diagnostic text is not promised unchanged.
145+
146+
The dedicated report step does not turn historical debt into a new mandatory gate. ``-PlookaheadFailOnFindings=true`` opts into failure when contexts need review; generation or analysis errors always fail. ``LookaheadAuditTest`` exercises changed dependencies, nullable/FOLLOW effects, stable anchors, opaque inputs and conservative handling; ordinary ``check`` runs these tests alongside the parser regressions. Full applicable checks and paired project JMH measurements are still required before publishing a cleanup.
147+
123148

124149
Commit a Pull Request
125150
---------------------------------
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2019 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.parser;
11+
12+
import static org.junit.jupiter.api.Assertions.*;
13+
import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed;
14+
import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
15+
import net.sf.jsqlparser.statement.ConnectionDefinition;
16+
import net.sf.jsqlparser.statement.CloudConnectionDefinition;
17+
import net.sf.jsqlparser.statement.export.DBMSDestination;
18+
import org.junit.jupiter.params.ParameterizedTest;
19+
import org.junit.jupiter.params.provider.ValueSource;
20+
21+
class LookaheadCleanupTest {
22+
@ParameterizedTest
23+
@ValueSource(strings = {"EXA AT conn TABLE t", "ORA AT conn TABLE schemaName.tableName",
24+
"JDBC DRIVER = 'driverName' AT conn TABLE \"t\"",
25+
"EXA AT conn TABLE t (c1, c2) REPLACE TRUNCATE"})
26+
void dispatchesTableDestinationsWithCompleteConsumption(String sql) throws Exception {
27+
CCJSqlParser parser = CCJSqlParserUtil.newParser(sql);
28+
DBMSDestination destination = parser.DBMSDestination();
29+
assertNotNull(destination.getTable());
30+
assertNull(destination.getStatement());
31+
assertNotNull(destination.getConnectionDefinition());
32+
assertEquals(CCJSqlParserConstants.EOF, parser.getToken(1).kind);
33+
assertSqlCanBeParsedAndDeparsed("EXPORT sourceTable INTO " + sql, true,
34+
p -> p.withDialect(Dialect.EXASOL));
35+
}
36+
37+
@ParameterizedTest
38+
@ValueSource(strings = {"EXA AT conn STATEMENT 'insert into t values (?)'",
39+
"ORA AT conn STATEMENT 'insert into t values (?)'"})
40+
void preservesStatementDestinations(String sql) throws Exception {
41+
CCJSqlParser parser = CCJSqlParserUtil.newParser(sql);
42+
DBMSDestination destination = parser.DBMSDestination();
43+
assertNull(destination.getTable());
44+
assertEquals("insert into t values (?)", destination.getStatement().getValue());
45+
assertEquals(CCJSqlParserConstants.EOF, parser.getToken(1).kind);
46+
assertSqlCanBeParsedAndDeparsed("EXPORT sourceTable INTO " + sql, true,
47+
p -> p.withDialect(Dialect.EXASOL));
48+
}
49+
50+
@ParameterizedTest
51+
@ValueSource(strings = {"EXA AT conn TABLE", "ORA AT conn STATEMENT", "EXA AT conn SELECT t"})
52+
void rejectsIncompleteAndWrongDestinationPrefixes(String sql) {
53+
CCJSqlParser parser = CCJSqlParserUtil.newParser(sql);
54+
assertThrows(ParseException.class, parser::DBMSDestination);
55+
if (sql.endsWith("TABLE")) {
56+
assertEquals(CCJSqlParserConstants.K_TABLE, parser.getToken(0).kind);
57+
assertEquals(CCJSqlParserConstants.EOF, parser.getToken(1).kind);
58+
}
59+
}
60+
61+
@ParameterizedTest
62+
@ValueSource(strings = {"AT conn", "AT '127.0.0.1'", "AT CLOUD NONE conn",
63+
"AT CLOUD AZURE BLOBSTORAGE conn"})
64+
void preservesCloudAndOrdinaryConnections(String sql) throws Exception {
65+
CCJSqlParser parser = CCJSqlParserUtil.newParser(sql);
66+
ConnectionDefinition connection = parser.ConnectionOrCloudConnectionDefinition();
67+
if (sql.startsWith("AT CLOUD")) {
68+
assertInstanceOf(CloudConnectionDefinition.class, connection);
69+
} else {
70+
assertEquals(ConnectionDefinition.class, connection.getClass());
71+
}
72+
assertEquals(CCJSqlParserConstants.EOF, parser.getToken(1).kind);
73+
assertSqlCanBeParsedAndDeparsed("EXPORT sourceTable INTO CSV " + sql + " FILE 'file.csv'",
74+
true, p -> p.withDialect(Dialect.EXASOL));
75+
}
76+
}

0 commit comments

Comments
 (0)