diff --git a/build.gradle b/build.gradle index a690cef6b..663909d70 100644 --- a/build.gradle +++ b/build.gradle @@ -11,6 +11,7 @@ buildscript { plugins { id 'java' + id "com.vanniktech.maven.publish" version "latest.release" id 'maven-publish' id 'signing' @@ -62,16 +63,17 @@ def getVersion = { boolean considerSnapshot -> snapshot = "-SNAPSHOT" } - return patch != null - ? "${major}.${minor}.${patch}${snapshot}" - : "${major}.${minor}${snapshot}" + return "${major}.${minor}" + + (patch != null ? ".${patch}" : "") + + (build != null ? ".${build}" : "") + + snapshot } // for publishing a release, call Gradle with Environment Variable RELEASE: // RELEASE=true gradle JSQLParser:publish version = getVersion( !System.getenv("RELEASE") ) -group = 'com.github.jsqlparser' +group = 'com.manticore-projects.jsqlformatter' description = 'JSQLParser library' tasks.register('generateBuildInfo') { @@ -130,6 +132,7 @@ tasks.withType(Checkstyle).configureEach { repositories { gradlePluginPortal() + mavenLocal() mavenCentral() // JavaCC 8 Snapshots @@ -170,21 +173,24 @@ dependencies { xmlDoclet ('com.manticore-projects.tools:xml-doclet:+'){ changing = true } // enforce latest version of JavaCC - testImplementation('org.javacc:core:8.1.0-SNAPSHOT') { changing = true } - testImplementation('org.javacc.generator:java:8.1.0-SNAPSHOT') { changing = true } + testImplementation('org.javacc:core:8.1.1-SNAPSHOT') { changing = true } + testImplementation('org.javacc.generator:java:8.1.1-SNAPSHOT') { changing = true } jmh 'org.openjdk.jmh:jmh-core:1.37' jmh 'org.openjdk.jmh:jmh-generator-annprocess:1.37' - javacc('org.javacc:core:8.1.0-SNAPSHOT') { changing = true } - javacc('org.javacc.generator:java:8.1.0-SNAPSHOT') { changing = true } + javacc('org.javacc:core:8.1.1-SNAPSHOT') { changing = true } + javacc('org.javacc.generator:java:8.1.1-SNAPSHOT') { changing = true } } configurations.configureEach { resolutionStrategy.eachDependency { DependencyResolveDetails details -> if (details.requested.group in ['org.javacc:core', 'org.javacc.generator']) { // Check for updates every build - resolutionStrategy.cacheChangingModulesFor 30, 'seconds' + resolutionStrategy.cacheChangingModulesFor 0, 'seconds' } } + + resolutionStrategy.force 'org.javacc:core:8.1.1-SNAPSHOT', + 'org.javacc.generator:java:8.1.1-SNAPSHOT' } compileJavacc { @@ -198,82 +204,9 @@ compileJavacc { ] } -// Post-process the generated CCJSqlParserTokenManager.java to split large static -// array initializers into separate methods, preventing the method from -// exceeding the JVM's 64KB bytecode limit (which breaks ASM-based tools like sbt-assembly). -tasks.register('splitTokenManagerStaticInit') { - dependsOn(compileJavacc) - - def tokenManagerFile = layout.buildDirectory.file( - "generated/javacc/net/sf/jsqlparser/parser/CCJSqlParserTokenManager.java" - ) - - inputs.file(tokenManagerFile) - outputs.file(tokenManagerFile) - - doLast { - def file = tokenManagerFile.get().asFile - if (!file.exists()) { - throw new GradleException("CCJSqlParserTokenManager.java not found at ${file}") - } - def content = file.text - - // Pattern matches static final array field declarations with inline initialization. - // We extract large ones and move their initialization into separate methods. - def fieldsToExtract = [ - // [regex-safe field name, array type for method return] - ['stringLiterals', 'int[]'], - ['jjstrLiteralImages', 'String[]'], - ['jjmatchKinds', 'int[]'], - ['jjnewLexState', 'int[]'], - ] - - fieldsToExtract.each { entry -> - def fieldName = entry[0] - def arrayType = entry[1] - - // Match: = { ... }; - // The field declaration may use 'public' or 'private' and 'static final' - def pattern = ~"(?s)((?:public|private)\\s+static\\s+final\\s+${java.util.regex.Pattern.quote(arrayType)}\\s+${fieldName}\\s*=\\s*)\\{(.*?)\\};" - def matcher = pattern.matcher(content) - if (matcher.find()) { - def prefix = matcher.group(1) - def body = matcher.group(2) - def methodName = "_init_${fieldName}" - def replacement = "${prefix}${methodName}();\n" + - " private static ${arrayType} ${methodName}() { return new ${arrayType} {${body}}; }" - content = matcher.replaceFirst(java.util.regex.Matcher.quoteReplacement(replacement)) - logger.lifecycle("splitTokenManagerStaticInit: extracted ${fieldName} initialization into ${methodName}()") - } - } - - // Handle int[][] arrays separately (jjcompositeState, jjnextStateSet) - def arrayArrayFields = ['jjcompositeState', 'jjnextStateSet'] - arrayArrayFields.each { fieldName -> - def pattern = ~"(?s)(private\\s+static\\s+final\\s+int\\[\\]\\[\\]\\s+${fieldName}\\s*=\\s*)\\{(.*?)\\};" - def matcher = pattern.matcher(content) - if (matcher.find()) { - def prefix = matcher.group(1) - def body = matcher.group(2) - def methodName = "_init_${fieldName}" - def replacement = "${prefix}${methodName}();\n" + - " private static int[][] ${methodName}() { return new int[][] {${body}}; }" - content = matcher.replaceFirst(java.util.regex.Matcher.quoteReplacement(replacement)) - logger.lifecycle("splitTokenManagerStaticInit: extracted ${fieldName} initialization into ${methodName}()") - } - } - - file.text = content - } -} - -tasks.withType(JavaCompile).configureEach { - dependsOn('splitTokenManagerStaticInit') -} - java { withSourcesJar() - withJavadocJar() + // withJavadocJar() sourceCompatibility = '11' targetCompatibility = '11' @@ -676,85 +609,56 @@ publish { dependsOn(check, gitChangelogTask, renderRR, xslt, xmldoc) } -publishing { - publications { - mavenJava(MavenPublication) { - artifactId = 'jsqlparser' +mavenPublishing { + publishToMavenCentral(true) + signAllPublications() - from components.java + coordinates(group, "jsqlparser", version) - versionMapping { - usage('java-api') { - fromResolutionOf('runtimeClasspath') - } - usage('java-runtime') { - fromResolutionResult() - } - } - - pom { - name.set('JSQLParser library') - description.set('Parse SQL Statements into Abstract Syntax Trees (AST)') - url.set('https://github.com/JSQLParser/JSqlParser') - - licenses { - license { - name.set('GNU Library or Lesser General Public License (LGPL) V2.1') - url.set('http://www.gnu.org/licenses/lgpl-2.1.html') - } - license { - name.set('The Apache Software License, Version 2.0') - url.set('http://www.apache.org/licenses/LICENSE-2.0.txt') - } - } + pom { + name.set('JSQLParser library') + description.set('Parse SQL Statements into Abstract Syntax Trees (AST)') + url.set('https://github.com/JSQLParser/JSqlParser') - developers { - developer { - id.set('twa') - name.set('Tobias Warneke') - email.set('t.warneke@gmx.net') - } - developer { - id.set('are') - name.set('Andreas Reichel') - email.set('andreas@manticore-projects.com') - } - } - - scm { - connection.set('scm:git:https://github.com/JSQLParser/JSqlParser.git') - developerConnection.set('scm:git:ssh://git@github.com:JSQLParser/JSqlParser.git') - url.set('https://github.com/JSQLParser/JSqlParser.git') - } + licenses { + license { + name.set('GNU Library or Lesser General Public License (LGPL) V2.1') + url.set('http://www.gnu.org/licenses/lgpl-2.1.html') + } + license { + name.set('The Apache Software License, Version 2.0') + url.set('http://www.apache.org/licenses/LICENSE-2.0.txt') } } - } - repositories { - maven { - name = "ossrh" - def releasesRepoUrl = "https://central.sonatype.com/repository/maven-releases" - def snapshotsRepoUrl = "https://central.sonatype.com/repository/maven-snapshots/" - url(version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl) - - credentials { - username = providers.environmentVariable("ossrhUsername").orNull - password = providers.environmentVariable("ossrhPassword").orNull + developers { + developer { + id.set('twa') + name.set('Tobias Warneke') + email.set('t.warneke@gmx.net') + } + developer { + id.set('are') + name.set('Andreas Reichel') + email.set('andreas@manticore-projects.com') } } + + scm { + connection.set('scm:git:https://github.com/JSQLParser/JSqlParser.git') + developerConnection.set('scm:git:ssh://git@github.com:JSQLParser/JSqlParser.git') + url.set('https://github.com/JSQLParser/JSqlParser.git') + } } } +// Fix signing task dependencies +tasks.withType(AbstractPublishToMaven).configureEach { + dependsOn(tasks.withType(Sign)) +} signing { - //def signingKey = findProperty("signingKey") - //def signingPassword = findProperty("signingPassword") - //useInMemoryPgpKeys(signingKey, signingPassword) - - // don't sign SNAPSHOTS - if (!version.endsWith('SNAPSHOT')) { - sign publishing.publications.mavenJava - } + required { !version.endsWith("SNAPSHOT") && gradle.taskGraph.hasTask("publish") } } tasks.withType(JavaCompile).configureEach { @@ -801,6 +705,17 @@ check { } jmh { + jmhVersion = '1.37' + jvmArgs = [ + "--enable-native-access=ALL-UNNAMED" + , "--add-opens=java.base/sun.misc=ALL-UNNAMED" + , "--add-opens=java.base/java.lang=ALL-UNNAMED" + , "-XX:+UnlockDiagnosticVMOptions" + , "-XX:+DebugNonSafepoints" + ] + + profilers = ['async:libPath=/opt/async-profiler/lib/libasyncProfiler.so;output=tree;dir=build/reports/jmh'] + includes = ['.*JSQLParserBenchmark.*'] warmupIterations = 2 fork = 3 diff --git a/src/main/java/net/sf/jsqlparser/parser/CCJSqlParserUtil.java b/src/main/java/net/sf/jsqlparser/parser/CCJSqlParserUtil.java index bdb25eeb4..31e070975 100644 --- a/src/main/java/net/sf/jsqlparser/parser/CCJSqlParserUtil.java +++ b/src/main/java/net/sf/jsqlparser/parser/CCJSqlParserUtil.java @@ -88,6 +88,8 @@ public static Statement parse(String sql, Consumer consumer) Statement statement; try { statement = parse(sql, executorService, consumer); + } catch (JSQLParserException ex) { + throw new JSQLParserException(sql, ex); } finally { executorService.shutdown(); } @@ -218,7 +220,7 @@ public static Expression parseExpression(String expressionStr, boolean allowPart "could only parse partial expression " + expression.toString()); } } catch (ParseException ex) { - throw new JSQLParserException(ex); + throw new JSQLParserException(expressionStr, ex); } } catch (JSQLParserException ex1) { // when fast simple parsing fails, try complex parsing but only if it has a chance to diff --git a/src/main/java/net/sf/jsqlparser/parser/SimpleCharStream.java b/src/main/java/net/sf/jsqlparser/parser/SimpleCharStream.java index c10c568a6..0e633ebbb 100644 --- a/src/main/java/net/sf/jsqlparser/parser/SimpleCharStream.java +++ b/src/main/java/net/sf/jsqlparser/parser/SimpleCharStream.java @@ -16,6 +16,7 @@ */ public class SimpleCharStream { + /** * Whether parser is static. */ @@ -24,8 +25,7 @@ public class SimpleCharStream { * Position in buffer. */ public int bufpos = -1; - protected int[] bufline; - protected int[] bufcolumn; + protected long[] buflinecolumn; protected int column = 0; protected int line = 1; protected boolean prevCharIsCR = false; @@ -52,8 +52,7 @@ public SimpleCharStream(Provider dstream, int startline, int startcolumn, int bu available = bufsize = buffersize; buffer = new char[buffersize]; - bufline = new int[buffersize]; - bufcolumn = new int[buffersize]; + buflinecolumn = new long[buffersize]; } /** @@ -83,43 +82,31 @@ public final int getAbsoluteTokenBegin() { } protected void ExpandBuff(boolean wrapAround) { - char[] newbuffer = new char[bufsize + 2048]; - int[] newbufline = new int[bufsize + 2048]; - int[] newbufcolumn = new int[bufsize + 2048]; - - try { - if (wrapAround) { - System.arraycopy(buffer, tokenBegin, newbuffer, 0, bufsize - tokenBegin); - System.arraycopy(buffer, 0, newbuffer, bufsize - tokenBegin, bufpos); - buffer = newbuffer; + final int newSize = bufsize * 2; + char[] newbuffer = new char[newSize]; + long[] newbuflinecolumn = new long[newSize]; - System.arraycopy(bufline, tokenBegin, newbufline, 0, bufsize - tokenBegin); - System.arraycopy(bufline, 0, newbufline, bufsize - tokenBegin, bufpos); - bufline = newbufline; + if (wrapAround) { + System.arraycopy(buffer, tokenBegin, newbuffer, 0, bufsize - tokenBegin); + System.arraycopy(buffer, 0, newbuffer, bufsize - tokenBegin, bufpos); + buffer = newbuffer; - System.arraycopy(bufcolumn, tokenBegin, newbufcolumn, 0, bufsize - tokenBegin); - System.arraycopy(bufcolumn, 0, newbufcolumn, bufsize - tokenBegin, bufpos); - bufcolumn = newbufcolumn; + System.arraycopy(buflinecolumn, tokenBegin, newbuflinecolumn, 0, bufsize - tokenBegin); + System.arraycopy(buflinecolumn, 0, newbuflinecolumn, bufsize - tokenBegin, bufpos); + buflinecolumn = newbuflinecolumn; - maxNextCharInd = bufpos += bufsize - tokenBegin; - } else { - System.arraycopy(buffer, tokenBegin, newbuffer, 0, bufsize - tokenBegin); - buffer = newbuffer; - - System.arraycopy(bufline, tokenBegin, newbufline, 0, bufsize - tokenBegin); - bufline = newbufline; + maxNextCharInd = bufpos += bufsize - tokenBegin; + } else { + System.arraycopy(buffer, tokenBegin, newbuffer, 0, bufsize - tokenBegin); + buffer = newbuffer; - System.arraycopy(bufcolumn, tokenBegin, newbufcolumn, 0, bufsize - tokenBegin); - bufcolumn = newbufcolumn; + System.arraycopy(buflinecolumn, tokenBegin, newbuflinecolumn, 0, bufsize - tokenBegin); + buflinecolumn = newbuflinecolumn; - maxNextCharInd = bufpos -= tokenBegin; - } - } catch (Throwable t) { - throw new Error(t.getMessage()); + maxNextCharInd = bufpos -= tokenBegin; } - - bufsize += 2048; + bufsize = newSize; available = bufsize; tokenBegin = 0; } @@ -165,7 +152,7 @@ protected void FillBuff() throws java.io.IOException { /** * Start. */ - public char BeginToken() throws java.io.IOException { + public final char BeginToken() throws java.io.IOException { tokenBegin = -1; char c = readChar(); tokenBegin = bufpos; @@ -175,9 +162,16 @@ public char BeginToken() throws java.io.IOException { return c; } - protected void UpdateLineColumn(char c) { + protected final void UpdateLineColumn(char c) { column++; + if (c >= ' ' && !prevCharIsLF && !prevCharIsCR) { + // Fast path: printable ASCII, no pending newline + buflinecolumn[bufpos] = ((long) line << 32) | (column & 0xFFFFFFFFL); + return; + } + + // Slow path: newlines, carriage returns, tabs, control chars if (prevCharIsLF) { prevCharIsLF = false; line += column = 1; @@ -199,20 +193,19 @@ protected void UpdateLineColumn(char c) { break; case '\t': column--; - column += tabSize - column % tabSize; + column += tabSize - (column % tabSize); break; default: break; } - bufline[bufpos] = line; - bufcolumn[bufpos] = column; + buflinecolumn[bufpos] = ((long) line << 32) | (column & 0xFFFFFFFFL); } /** * Read a character. */ - public char readChar() throws java.io.IOException { + public final char readChar() throws java.io.IOException { if (inBuf > 0) { --inBuf; @@ -244,7 +237,7 @@ public char readChar() throws java.io.IOException { */ public int getColumn() { - return bufcolumn[bufpos]; + return (int) buflinecolumn[bufpos]; } @Deprecated @@ -254,42 +247,41 @@ public int getColumn() { */ public int getLine() { - return bufline[bufpos]; + return (int) (buflinecolumn[bufpos] >>> 32); } /** * Get token end column number. */ public int getEndColumn() { - return bufcolumn[bufpos]; + return (int) buflinecolumn[bufpos]; } /** * Get token end line number. */ public int getEndLine() { - return bufline[bufpos]; + return (int) (buflinecolumn[bufpos] >>> 32); } /** * Get token beginning column number. */ public int getBeginColumn() { - return bufcolumn[tokenBegin]; + return (int) buflinecolumn[tokenBegin]; } /** * Get token beginning line number. */ public int getBeginLine() { - return bufline[tokenBegin]; + return (int) (buflinecolumn[tokenBegin] >>> 32); } /** * Backup a number of characters. */ - public void backup(int amount) { - + public final void backup(int amount) { inBuf += amount; totalCharsRead -= amount; if ((bufpos -= amount) < 0) { @@ -308,8 +300,7 @@ public void ReInit(Provider dstream, int startline, int startcolumn, int buffers if (buffer == null || buffersize != buffer.length) { available = bufsize = buffersize; buffer = new char[buffersize]; - bufline = new int[buffersize]; - bufcolumn = new int[buffersize]; + buflinecolumn = new long[buffersize]; } prevCharIsLF = prevCharIsCR = false; tokenBegin = inBuf = maxNextCharInd = 0; @@ -330,7 +321,6 @@ public void ReInit(Provider dstream) { ReInit(dstream, 1, 1, 4096); } - /** * Get token literal value. */ @@ -364,8 +354,7 @@ public char[] GetSuffix(int len) { */ public void Done() { buffer = null; - bufline = null; - bufcolumn = null; + buflinecolumn = null; } /** @@ -387,29 +376,34 @@ public void adjustBeginLineColumn(int newLine, int newCol) { int nextColDiff; int columnDiff = 0; - while (i < len && bufline[j = start % bufsize] == bufline[k = ++start % bufsize]) { - bufline[j] = newLine; - nextColDiff = columnDiff + bufcolumn[k] - bufcolumn[j]; - bufcolumn[j] = newCol + columnDiff; + while (i < len && + (int) (buflinecolumn[j = start % bufsize] >>> 32) == (int) (buflinecolumn[k = + ++start % bufsize] >>> 32)) { + int colJ = (int) buflinecolumn[j]; + int colK = (int) buflinecolumn[k]; + nextColDiff = columnDiff + colK - colJ; + buflinecolumn[j] = ((long) newLine << 32) | ((newCol + columnDiff) & 0xFFFFFFFFL); columnDiff = nextColDiff; i++; } if (i < len) { - bufline[j] = newLine++; - bufcolumn[j] = newCol + columnDiff; + buflinecolumn[j] = ((long) (newLine++) << 32) | ((newCol + columnDiff) & 0xFFFFFFFFL); while (i++ < len) { - if (bufline[j = start % bufsize] != bufline[++start % bufsize]) { - bufline[j] = newLine++; + int lineJ = (int) (buflinecolumn[j = start % bufsize] >>> 32); + int lineNext = (int) (buflinecolumn[++start % bufsize] >>> 32); + if (lineJ != lineNext) { + buflinecolumn[j] = + ((long) (newLine++) << 32) | (buflinecolumn[j] & 0xFFFFFFFFL); } else { - bufline[j] = newLine; + buflinecolumn[j] = ((long) newLine << 32) | (buflinecolumn[j] & 0xFFFFFFFFL); } } } - line = bufline[j]; - column = bufcolumn[j]; + line = (int) (buflinecolumn[j] >>> 32); + column = (int) buflinecolumn[j]; } boolean getTrackLineColumn() { diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index d290ced5e..9473411b6 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -212,16 +212,6 @@ public class CCJSqlParser extends AbstractJSqlParser { } } - protected boolean isParenthesedSelectAhead() { - if (getToken(1).kind != OPENING_BRACKET) { - return false; - } - int nextKind = getToken(2).kind; - return nextKind == K_SELECT - || nextKind == K_WITH - || nextKind == K_VALUES - || nextKind == K_FROM; - } /** * Tokens that have dedicated branches in PrimaryExpression AFTER the Function branch. @@ -246,6 +236,7 @@ public class CCJSqlParser extends AbstractJSqlParser { * Replaces LOOKAHEAD(16) on Function() with a targeted O(chain-length) check. */ protected boolean isFunctionAhead() { + try { int i = 1; Token t = getToken(i); @@ -298,6 +289,223 @@ public class CCJSqlParser extends AbstractJSqlParser { } return true; + } catch (TokenMgrError e) { + return false; + } + } + + /** + * Pratt arithmetic operator precedence loop. + * Handles: *, /, %, ^, DIV (prec=6) and +, -, ||, |, &, <<, >> (prec=5) + * Called only in real-parse mode (not syntactic-LOOKAHEAD mode) so action + * blocks execute correctly. Safe now that no syntactic production LOOKAHEADs + * scan through arithmetic expressions. + */ + /** + * Pratt boolean operator precedence loop. + * Handles OR (prec=2), XOR (prec=3), AND/&& (prec=4). + * Safe to use now that no syntactic production LOOKAHEADs scan through expressions. + */ + protected Expression prattExpressionRest(Expression left, int minPrec) throws ParseException { + while (!interrupted) { + int op = getToken(1).kind; + int prec; + if (op == K_AND || op == OP_DOUBLEAND) prec = 4; + else if (op == K_XOR || op == K_OR) prec = 2; + else break; + if (prec < minPrec) break; + jj_consume_token(op, getToken(1).image); + // +1 makes OR/AND/XOR left-associative + Expression right = prattExpressionRest(Condition(), prec + 1); + if (op == K_AND) { + left = new AndExpression(left, right); + } else if (op == OP_DOUBLEAND) { + AndExpression a = new AndExpression(left, right); + a.setUseOperator(true); left = a; + } else if (op == K_XOR) { + left = new XorExpression(left, right); + } else { + left = new OrExpression(left, right); + } + } + return left; + } + + protected Expression prattArithRest(Expression left, int minPrec) throws ParseException { + while (!interrupted) { + Token t = getToken(1); + int op = t.kind; + int prec; + + // Named tokens: OP_SLASH(/), OP_CARET(^), K_DIV, OP_CONCAT(||), + // OP_PIPE(|), OP_LSHIFT(<<), OP_RSHIFT(>>) + // String-literal tokens: *, +, -, %, & (unnamed in JavaCC grammar) + if (op == OP_SLASH || op == OP_CARET || op == K_DIV) prec = 6; + else if (op == OP_CONCAT || op == OP_PIPE + || op == OP_LSHIFT || op == OP_RSHIFT) prec = 5; + else { + // Handle unnamed tokens by image + String img = t.image; + if ("*".equals(img) || "%".equals(img)) prec = 6; + else if ("+".equals(img) || "-".equals(img) || "&".equals(img)) prec = 5; + else break; // not an arithmetic operator + } + + if (prec < minPrec) break; + + // OP_PIPE: distinguish " | | " (space-sep concat) from single " | " (bitwise OR) + if (op == OP_PIPE && getToken(2).kind == OP_PIPE) { + jj_consume_token(OP_PIPE, getToken(1).image); + jj_consume_token(OP_PIPE, getToken(1).image); + Expression right = prattArithRest(PrimaryExpression(), prec + 1); + Concat r = new Concat(); r.setLeftExpression(left); r.setRightExpression(right); left = r; + continue; + } + + jj_consume_token(op, getToken(1).image); + Expression right = prattArithRest(PrimaryExpression(), prec + 1); + + if (op == OP_SLASH) { Division r = new Division(); r.setLeftExpression(left); r.setRightExpression(right); left = r; } + else if (op == OP_CARET) { net.sf.jsqlparser.expression.operators.arithmetic.BitwiseXor r = new net.sf.jsqlparser.expression.operators.arithmetic.BitwiseXor(); r.setLeftExpression(left); r.setRightExpression(right); left = r; } + else if (op == K_DIV) { IntegerDivision r = new IntegerDivision(); r.setLeftExpression(left); r.setRightExpression(right); left = r; } + else if (op == OP_CONCAT) { Concat r = new Concat(); r.setLeftExpression(left); r.setRightExpression(right); left = r; } + else if (op == OP_PIPE) { BitwiseOr r = new BitwiseOr(); r.setLeftExpression(left); r.setRightExpression(right); left = r; } + else if (op == OP_LSHIFT) { BitwiseLeftShift r = new BitwiseLeftShift(); r.setLeftExpression(left); r.setRightExpression(right); left = r; } + else if (op == OP_RSHIFT) { BitwiseRightShift r = new BitwiseRightShift(); r.setLeftExpression(left); r.setRightExpression(right); left = r; } + else { + String img = t.image; + if ("*".equals(img)) { Multiplication r = new Multiplication(); r.setLeftExpression(left); r.setRightExpression(right); left = r; } + else if ("+".equals(img)) { Addition r = new Addition(); r.setLeftExpression(left); r.setRightExpression(right); left = r; } + else if ("-".equals(img)) { Subtraction r = new Subtraction(); r.setLeftExpression(left); r.setRightExpression(right); left = r; } + else if ("%".equals(img)) { Modulo r = new Modulo(); r.setLeftExpression(left); r.setRightExpression(right); left = r; } + else if ("&".equals(img)) { BitwiseAnd r = new BitwiseAnd(); r.setLeftExpression(left); r.setRightExpression(right); left = r; } + } + } + return left; + } + + // True when "(" opens a parenthesised FROM item (table/function/lateral/join) + // rather than a SELECT subquery. Cheap 2-token check replaces expensive + // syntactic LOOKAHEAD(ParenthesedFromItem()). + protected boolean isParenthesedFromItemAhead() { + try { + if (getToken(1).kind != OPENING_BRACKET) return false; + int k2 = getToken(2).kind; + // Direct subquery: ( SELECT/WITH ) → ParenthesedSelect handles it + // ( VALUES ) can be ParenthesedFromItem(Values) or ParenthesedSelect; + // ParenthesedFromItem is checked first so Values-as-FROM-item works + if (k2 == K_SELECT || k2 == K_WITH) return false; + // ( ( SELECT ) UNION ( SELECT ) ) → isNestedSetOperationAhead handles it + // Other (( patterns: ( (SELECT...) JOIN ... ) or ( (table) ) → ParenthesedFromItem + if (k2 == K_LATERAL) return false; // handled by LateralSubSelect + if (k2 == CLOSING_BRACKET) return false; // empty "()" is not valid + // EXASOL: ( IMPORT ... ) is a SubImport; handled at the outer FromItem level + // by LOOKAHEAD(2, Dialect.EXASOL...) SubImport() - must NOT go into ParenthesedFromItem + if (k2 == K_IMPORT) return false; + return true; + } catch (TokenMgrError e) { return false; } + } + + // True when ( (SELECT ...) UNION/INTERSECT/EXCEPT (SELECT ...) ) — a set-operation + // subquery used as IN RHS. Bracket-scans past the inner select to verify a set + // operator follows, distinguishing from comma-separated ((SELECT...), (SELECT...)). + protected boolean isNestedSetOperationAhead() { + try { + if (getToken(1).kind != OPENING_BRACKET) return false; + if (getToken(2).kind != OPENING_BRACKET) return false; + // Skip any run of opening brackets: ( ( ( ... ( SELECT + int i = 3; + while (getToken(i).kind == OPENING_BRACKET) i++; + int k = getToken(i).kind; + if (k != K_SELECT && k != K_WITH && k != K_VALUES) return false; + // Scan forward counting brackets to find the matching ) for token(2) + // We are now inside all the opening brackets; depth = number of brackets skipped - 1 + // (token(1) is the outer one, token(2..i-1) are the inner ones we scanned) + int depth = i - 2; // brackets from position 2 to i-1 inclusive + i++; + while (i <= 500) { + Token t; try { t = getToken(i); } catch (TokenMgrError e) { return false; } + if (t == null || t.kind == 0) return false; + if (t.kind == OPENING_BRACKET) depth++; + else if (t.kind == CLOSING_BRACKET) { + if (--depth == 0) break; + } + i++; + } + if (i > 500) return false; + // Token at position i+1 follows the innermost closing ) + Token next; try { next = getToken(i + 1); } catch (TokenMgrError e) { return false; } + if (next == null) return false; + return next.kind == K_UNION || next.kind == K_INTERSECT || + next.kind == K_EXCEPT || next.kind == K_MINUS; + } catch (TokenMgrError e) { return false; } + } + + protected boolean isParenthesedSelectAhead() { + try { + if (getToken(1).kind != OPENING_BRACKET) return false; + int k = getToken(2).kind; + return k == K_SELECT || k == K_WITH || k == K_VALUES; + } catch (TokenMgrError e) { + return false; + } + } + + protected boolean isImplicitCastAhead() { + try { + int k1 = getToken(1).kind; + // DT_ZONE (TIMESTAMP WITH TIME ZONE / WITHOUT TIME ZONE) is also a valid + // implicit cast type prefix handled by DataType(), but distinct from DATA_TYPE. + if (k1 == DT_ZONE) return true; + if (k1 != DATA_TYPE) return false; + int k2 = getToken(2).kind; + if (k2 != OPENING_BRACKET) return true; // DATA_TYPE literal - simple cast + // DATA_TYPE( ... ) - precision cast if content is only S_LONG literals + // function call otherwise (e.g. UUID(), VARCHAR(col)) + int k3 = getToken(3).kind; + if (k3 == CLOSING_BRACKET) return false; // DATA_TYPE() - empty call + if (k3 != S_LONG) return false; // DATA_TYPE(expr) - function call + int k4 = getToken(4).kind; + if (k4 == CLOSING_BRACKET) return true; // DATA_TYPE(N) - precision cast + if (k4 != K_COMMA) return false; // DATA_TYPE(N expr) - function call + int k5 = getToken(5).kind; + if (k5 != S_LONG) return false; // DATA_TYPE(N, expr) - function call + return getToken(6).kind == CLOSING_BRACKET; // DATA_TYPE(N,M) - precision cast + } catch (TokenMgrError e) { + return false; + } + } + + protected boolean isCaseExpressionAhead() { + try { + if (getToken(1).kind != K_CASE) return false; + int k2 = getToken(2).kind; + // CASE WHEN ... is unambiguously a CASE expression + if (k2 == K_WHEN) return true; + // CASE followed by tokens that only appear after an expression means + // CASE is being used as a column/table name identifier + if (k2 == K_FROM || k2 == K_WHERE || k2 == K_AS || + k2 == K_AND || k2 == K_OR || + k2 == K_IS || k2 == K_IN || k2 == K_LIKE || k2 == K_BETWEEN || + k2 == K_THEN || k2 == K_ELSE || k2 == K_END || + k2 == K_GROUP || k2 == K_ORDER || k2 == K_HAVING || k2 == K_LIMIT || + k2 == K_UNION || k2 == K_INTERSECT || k2 == K_EXCEPT || + k2 == K_OVER || k2 == CLOSING_BRACKET || k2 == 0) return false; + // Also check single-char operator tokens by image + Token t2 = getToken(2); + if (t2 != null && t2.image != null) { + String img = t2.image; + if (img.length() == 1) { + char c = img.charAt(0); + if (c == '=' || c == '>' || c == '<' || c == '+' || c == '-' || + c == '*' || c == ',' || c == '.' || c == ';') return false; + } + } + // Otherwise assume it is a CASE expression with a switch value + return true; + } catch (TokenMgrError e) { + return false; + } } private boolean isKeywordArgumentAhead() { @@ -638,10 +846,12 @@ public class CCJSqlParser extends AbstractJSqlParser { * in Condition(), eliminating choice conflicts. */ protected boolean isConditionSuffixAhead() { - if (isComparisonOperatorAhead()) { - return true; - } Token t = getToken(1); + // Inline isComparisonOperatorAhead for single getToken(1) call + if (t.kind == OPENING_BRACKET && getToken(2).image.equals("+")) { + return isComparisonOperator(getToken(4)); + } + if (isComparisonOperator(t)) return true; switch (t.kind) { // Each suffix's start token: case K_OVERLAPS: // OVERLAPS @@ -675,6 +885,8 @@ public class CCJSqlParser extends AbstractJSqlParser { return false; } } + + } PARSER_END(CCJSqlParser) @@ -1307,8 +1519,13 @@ TOKEN : /* Operators */ | )* "="> | )* ">"> | )* "="> -| )* "="> -| )* "|"> +| +| +| +| +| +| >"> | | "> | @@ -5376,11 +5593,7 @@ SelectItem SelectItem() #SelectItem: ( LOOKAHEAD( 3 ) expression = ConnectByPriorOperator() | - LOOKAHEAD( 3 ) expression = XorExpression() - | - LOOKAHEAD( 3 ) expression = ConcatExpression() - | - expression=Expression() + ( expression=Condition() { expression = prattExpressionRest(expression, 2); } ) ) [ LOOKAHEAD({ isAliasAhead() }) alias=Alias() ] { @@ -5860,7 +6073,13 @@ FromItem FromItem() #FromItem: | LOOKAHEAD(3) fromItem=Table() | - LOOKAHEAD(ParenthesedFromItem()) fromItem = ParenthesedFromItem() + LOOKAHEAD({ isNestedSetOperationAhead() }) ( + fromItem=ParenthesedSelect() + [ LOOKAHEAD(2) pivot=Pivot() { fromItem.setPivot(pivot); } ] + [ LOOKAHEAD(2) unpivot=UnPivot() { fromItem.setUnPivot(unpivot); } ] + ) + | + LOOKAHEAD({ isParenthesedFromItemAhead() }) fromItem = ParenthesedFromItem() | LOOKAHEAD(3, { !getAsBoolean(Feature.allowUnparenthesizedSubSelects) }) ( fromItem=ParenthesedSelect() @@ -6578,7 +6797,7 @@ Top Top() #Top: [ LOOKAHEAD(2) token = { ((JdbcNamedParameter)top.getExpression()).setName(token.image); } ] | "(" - expr=AdditiveExpression() + expr=SimpleExpression() { top.setExpression(expr); top.setParenthesis(true); @@ -6661,7 +6880,8 @@ Expression Expression() #Expression : Expression expression = null; } { - expression=XorExpression() + expression=Condition() + { expression = prattExpressionRest(expression, 2); } { linkAST(expression,jjtThis); return expression; @@ -6670,70 +6890,44 @@ Expression Expression() #Expression : Expression XorExpression(): { - Expression left, right, result; -} -{ - left=OrExpression() { result = left; } - ( LOOKAHEAD(2) - - right=OrExpression() - { - result = new XorExpression(left, right); - left = result; - } - )* - { - return result; - } -} - -Expression OrExpression(): -{ - Expression left, right, result; + Expression result; + Expression right; } { - left=AndExpression() { result = left; } - ( LOOKAHEAD(2) - - right=AndExpression() - { - result = new OrExpression(left, right); - left = result; - } - )* - { - return result; - } - + result=AndChain() + ( + LOOKAHEAD(2) + ( + right=AndChain() { result = new OrExpression(result, right); } + | + right=AndChain() { result = new XorExpression(result, right); } + ) + )* + { return result; } } -Expression AndExpression() : +/** AND — higher precedence than OR/XOR */ +Expression AndChain(): { - Expression left, right, result; + Expression result; + Expression right; } { - // ParenthesedExpressionList always delegates to ComplexExpressionList which - // uses full Expression(), so Condition() can handle ALL parenthesized content - // (including boolean operators like LIKE/IN/BETWEEN/IS inside parens). - // No speculative parsing or XorExpression fallback needed. - left=Condition() - { result = left; } - - ( LOOKAHEAD(2) - { boolean useOperator = false; } - ( | {useOperator=true;} ) - - right=Condition() - - { - result = new AndExpression(left, right); - ((AndExpression)result).setUseOperator(useOperator); - left = result; - } + result=Condition() + ( + LOOKAHEAD(2) + ( + right=Condition() {{ + AndExpression a = new AndExpression(result, right); result = a; + }} + | + right=Condition() {{ + AndExpression a = new AndExpression(result, right); + a.setUseOperator(true); result = a; + }} + ) )* - { - return result; - } + { return result; } } Expression Condition(): @@ -6746,16 +6940,16 @@ Expression Condition(): int oracleJoin = EqualsTo.NO_ORACLE_JOIN; } { - [ LOOKAHEAD(2) ( { not=true; } | "!" { not=true; exclamationMarkNot=true; })] + [ LOOKAHEAD(2, { getToken(1).kind == K_NOT || "!".equals(getToken(1).image) }) ( { not=true; } | "!" { not=true; exclamationMarkNot=true; })] ( result=ExistsExpression() | - [ LOOKAHEAD(2) { oraclePrior = EqualsTo.ORACLE_PRIOR_START; } ] + [ LOOKAHEAD(2, { getToken(1).kind == K_PRIOR }) { oraclePrior = EqualsTo.ORACLE_PRIOR_START; } ] left=SimpleExpression() { result = left; } // Consume Oracle (+) once, before dispatching [ - LOOKAHEAD("(" "+" ")") + LOOKAHEAD("(" "+" ")", { getToken(1).kind == OPENING_BRACKET }) "(" "+" ")" { oracleJoin = EqualsTo.ORACLE_JOIN_RIGHT; @@ -6938,6 +7132,8 @@ Expression InExpression(Expression leftExpression) #InExpression : ( LOOKAHEAD(2) token= { rightExpression = new StringValue(token.image); } | + LOOKAHEAD({ isNestedSetOperationAhead() }) rightExpression = ParenthesedSelect() + | rightExpression = PrimaryExpression() ) { @@ -7362,15 +7558,11 @@ Expression ComparisonItem() : } { ( - LOOKAHEAD( 6 ) retval=AnyComparisonExpression() - | - LOOKAHEAD( 3 ) retval=SimpleExpression() - | - LOOKAHEAD( 3 ) retval=ParenthesedExpressionList() + LOOKAHEAD(6, { getToken(1).kind == K_ANY || getToken(1).kind == K_SOME || getToken(1).kind == K_ALL }) retval=AnyComparisonExpression() | - LOOKAHEAD( 3 ) retval=RowConstructor() + LOOKAHEAD({ getToken(1).kind == K_ROW }) retval=RowConstructor() | - retval=PrimaryExpression() + retval=SimpleExpression() ) { @@ -7404,8 +7596,9 @@ Expression SimpleExpression(): Token operation = null; } { - [ LOOKAHEAD( 5 ) user = UserVariable() ( operation = "=" | operation = ":=" ) ] - retval=ConcatExpression() + [ LOOKAHEAD( 5, { getToken(1).kind == S_AT_IDENTIFIER } ) user = UserVariable() ( operation = "=" | operation = ":=" ) ] + retval=PrimaryExpression() + { retval = prattArithRest(retval, 5); } { if (user != null) { VariableAssignment assignment = new VariableAssignment(); @@ -7418,136 +7611,60 @@ Expression SimpleExpression(): } } -Expression ConcatExpression(): -{ - Expression result = null; - Expression leftExpression = null; - Expression rightExpression = null; -} -{ - leftExpression=BitwiseAndOr() { result = leftExpression; } - (LOOKAHEAD(3) - /* Oracle allows space between the bars. */ - rightExpression=BitwiseAndOr() - { - Concat binExp = new Concat(); - binExp.setLeftExpression(leftExpression); - binExp.setRightExpression(rightExpression); - result = binExp; - leftExpression = result; - } - )* - - { return result; } -} - -Expression BitwiseAndOr(): +/** Multiplicative/bitwise-shift — higher precedence */ +Expression MulChain(): { - Expression result = null; - Expression leftExpression = null; - Expression rightExpression = null; + Expression retval; + Expression right; } { - leftExpression=AdditiveExpression() { result = leftExpression; } + retval=PrimaryExpression() ( - LOOKAHEAD(2) ( - "|" { result = new BitwiseOr(); } + LOOKAHEAD(2) + ( + "*" right=PrimaryExpression() {{ Multiplication r = new Multiplication(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }} + | + right=PrimaryExpression() {{ Division r = new Division(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }} + | + "%" right=PrimaryExpression() {{ Modulo r = new Modulo(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }} | - "&" { result = new BitwiseAnd(); } - | - "<<" { result = new BitwiseLeftShift(); } - | - ">>" { result = new BitwiseRightShift(); } + right=PrimaryExpression() {{ net.sf.jsqlparser.expression.operators.arithmetic.BitwiseXor r = new net.sf.jsqlparser.expression.operators.arithmetic.BitwiseXor(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }} + | + right=PrimaryExpression() {{ IntegerDivision r = new IntegerDivision(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }} ) - - rightExpression=AdditiveExpression() - - { - BinaryExpression binExp = (BinaryExpression) result; - binExp.setLeftExpression(leftExpression); - binExp.setRightExpression(rightExpression); - leftExpression = result; - } - )* - - { return result; } -} - -Expression AdditiveExpression(): -{ - Expression result = null; - Expression leftExpression = null; - Expression rightExpression = null; -} -{ - leftExpression=MultiplicativeExpression() { result = leftExpression; } - ( LOOKAHEAD(2) - ("+" { result = new Addition(); } - | "-" { result = new Subtraction(); } ) - - rightExpression=MultiplicativeExpression() - { - BinaryExpression binExp = (BinaryExpression) result; - binExp.setLeftExpression(leftExpression); - binExp.setRightExpression(rightExpression); - leftExpression = result; - } )* - - { return result; } + { return retval; } } -Expression MultiplicativeExpression(): +/** Additive/concat/bitwise — lower precedence */ +Expression AddChain(): { - Expression result = null; - Expression leftExpression = null; - Expression rightExpression = null; + Expression retval; + Expression right; } { + retval=MulChain() ( - leftExpression=BitwiseXor() - ) - { result = leftExpression; } - ( - LOOKAHEAD(2) ("*" { result = new Multiplication(); } - | "/" { result = new Division(); } - | { result = new IntegerDivision(); } - | "%" { result = new Modulo(); } - ) - - rightExpression=BitwiseXor() - - { - BinaryExpression binExp = (BinaryExpression) result; - binExp.setLeftExpression(leftExpression); - binExp.setRightExpression(rightExpression); - leftExpression = result; - } - )* - { return result; } -} - -Expression BitwiseXor(): -{ - Expression result = null; - Expression leftExpression = null; - Expression rightExpression = null; -} -{ - leftExpression=PrimaryExpression() { result = leftExpression; } - ( LOOKAHEAD(2) - "^" - rightExpression=PrimaryExpression() - { - BitwiseXor binExp = new BitwiseXor(); - binExp.setLeftExpression(leftExpression); - binExp.setRightExpression(rightExpression); - result = binExp; - leftExpression = result; - } + LOOKAHEAD(2) + ( + "+" right=MulChain() {{ Addition r = new Addition(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }} + | + "-" right=MulChain() {{ Subtraction r = new Subtraction(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }} + | + right=MulChain() {{ Concat r = new Concat(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }} + | + LOOKAHEAD(2) right=MulChain() {{ Concat r = new Concat(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }} + | + right=MulChain() {{ BitwiseOr r = new BitwiseOr(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }} + | + "&" right=MulChain() {{ BitwiseAnd r = new BitwiseAnd(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }} + | + right=MulChain() {{ BitwiseLeftShift r = new BitwiseLeftShift(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }} + | + right=MulChain() {{ BitwiseRightShift r = new BitwiseRightShift(); r.setLeftExpression(retval); r.setRightExpression(right); retval = r; }} + ) )* - - { return result; } + { return retval; } } Expression ArrayExpression(Expression obj): { @@ -7601,11 +7718,11 @@ Expression PrimaryExpression() #PrimaryExpression: ( { retval = new NullValue(); } - | LOOKAHEAD(3, {!interrupted}) retval=CaseWhenExpression() + | LOOKAHEAD({!interrupted && isCaseExpressionAhead()}) retval=CaseWhenExpression() | LOOKAHEAD(2, {!interrupted}) retval=CharacterPrimary() - | LOOKAHEAD(6, {!interrupted}) retval=ImplicitCast() + | LOOKAHEAD({!interrupted && isImplicitCastAhead()}) retval=ImplicitCast() | retval = JdbcParameter() @@ -7686,9 +7803,13 @@ Expression PrimaryExpression() #PrimaryExpression: | "{ts" token= "}" { retval = new TimestampValue(token.image); } - | LOOKAHEAD( Select() , { getAsBoolean(Feature.allowUnparenthesizedSubSelects) && !interrupted } ) retval=Select() + | LOOKAHEAD({ getAsBoolean(Feature.allowUnparenthesizedSubSelects) && !interrupted + && (getToken(1).kind == K_SELECT || getToken(1).kind == K_WITH || getToken(1).kind == K_VALUES + || (getToken(1).kind == OPENING_BRACKET && (getToken(2).kind == K_SELECT || getToken(2).kind == K_WITH || getToken(2).kind == K_VALUES))) }) retval=Select() + + | LOOKAHEAD({ !getAsBoolean(Feature.allowUnparenthesizedSubSelects) && !interrupted && isParenthesedSelectAhead() }) retval=ParenthesedSelect() - | LOOKAHEAD( ParenthesedSelect() , { !getAsBoolean(Feature.allowUnparenthesizedSubSelects) && !interrupted } ) retval=ParenthesedSelect() + | LOOKAHEAD({ !interrupted && isNestedSetOperationAhead() }) retval=ParenthesedSelect() | ( diff --git a/src/site/sphinx/javadoc_snapshot.rst b/src/site/sphinx/javadoc_snapshot.rst new file mode 100644 index 000000000..c5c1dce2b --- /dev/null +++ b/src/site/sphinx/javadoc_snapshot.rst @@ -0,0 +1,8 @@ +####################################################################### +JSQLParser Java API Snapshot +####################################################################### + +Base Package: net.sf.jsqlparser + +.. javadoc-api:: javadoc_snapshot.xml + :public-only: \ No newline at end of file diff --git a/src/site/sphinx/javadoc_stable.rst b/src/site/sphinx/javadoc_stable.rst new file mode 100644 index 000000000..d9980a745 --- /dev/null +++ b/src/site/sphinx/javadoc_stable.rst @@ -0,0 +1,8 @@ +####################################################################### +JSQLParser Java API Stable +####################################################################### + +Base Package: net.sf.jsqlparser + +.. javadoc-api:: javadoc_stable.xml + :public-only: \ No newline at end of file diff --git a/src/site/sphinx/syntax_stable.rst b/src/site/sphinx/syntax_stable.rst index 92839aa66..29cb15c1c 100644 --- a/src/site/sphinx/syntax_stable.rst +++ b/src/site/sphinx/syntax_stable.rst @@ -13,7 +13,7 @@ NonReservedWord .. raw:: html - + @@ -210,528 +210,530 @@ NonReservedWord ENCLOSED EMIT - - ENABLE - - ENCODING - - ENCRYPTION - - END - - ESCAPED - - ENFORCED - - ENGINE - - ERROR - - ESCAPE - - EXA - - EXCHANGE - - EXCLUDE - - EXCLUDING - - EXCLUSIVE - - EXEC - - EXECUTE - - EXPLAIN - - EXPLICIT - - EXTENDED - - EXTRACT - - EXPORT - - K_ISOLATION - FILTER - - FIELDS - - FIRST - - FLUSH - - FOLLOWING - - FORMAT - - FULLTEXT - - FUNCTION - - GRANT - - GROUP_CONCAT - - GUARD - - HASH - - HIGH - - HIGH_PRIORITY - - HISTORY - - HOPPING - - IDENTIFIED - - IDENTITY - - INCLUDE - - INCLUDE_NULL_VALUES - - INCLUDING - - INCREMENT - - INDEX - - INFORMATION - - INSERT - - INTERLEAVE - - INTERPRET - - INVALIDATE - - INVERSE - - INVISIBLE - - ISNULL - - JDBC - - JSON - - JSON_OBJECT - - JSON_OBJECTAGG - - JSON_ARRAY - - JSON_ARRAYAGG - - KEEP - - KEY_BLOCK_SIZE - - KEY - - KEYS - - KILL - - FN - - LAST - - LEADING - - LESS - - LEVEL + + EMPTY + + ENABLE + + ENCODING + + ENCRYPTION + + END + + ESCAPED + + ENFORCED + + ENGINE + + ERROR + + ESCAPE + + EXA + + EXCHANGE + + EXCLUDE + + EXCLUDING + + EXCLUSIVE + + EXEC + + EXECUTE + + EXPLAIN + + EXPLICIT + + EXTENDED + + EXTRACT + + EXPORT + + K_ISOLATION + FILTER + + FIELDS + + FIRST + + FLUSH + + FOLLOWING + + FORMAT + + FULLTEXT + + FUNCTION + + GRANT + + GROUP_CONCAT + + GUARD + + HASH + + HIGH + + HIGH_PRIORITY + + HISTORY + + HOPPING + + IDENTIFIED + + IDENTITY + + INCLUDE + + INCLUDE_NULL_VALUES + + INCLUDING + + INCREMENT + + INDEX + + INFORMATION + + INSERT + + INTERLEAVE + + INTERPRET + + INVALIDATE + + INVERSE + + INVISIBLE + + ISNULL + + JDBC + + JSON + + JSON_OBJECT + + JSON_OBJECTAGG + + JSON_ARRAY + + JSON_ARRAYAGG + + KEEP + + KEY_BLOCK_SIZE + + KEY + + KEYS + + KILL + + FN + + LAST + + LEADING + + LESS - LINES - - LOCAL - - LOCK - - LOCKED - - LINK - - LOG - - LOOP - - LOW - - LOW_PRIORITY - - LTRIM - - MATCH - - MATCH_ANY - - MATCH_ALL - - MATCH_PHRASE - - MATCH_PHRASE_PREFIX - - MATCH_REGEXP - - MATCHED - - MATERIALIZED - - MAX - - MAXVALUE - - MEMBER - - MERGE - - MIN - - MINVALUE - - MODE - - MODIFY - - MOVEMENT - - NAMES - - NAME - - NEVER - - NEXT - - K_NEXTVAL - NO - - NOCACHE - - NOKEEP - - NOLOCK - - NOMAXVALUE - - NOMINVALUE - - NONE - - NOORDER - - NOTHING + LEVEL + + LINES + + LOCAL + + LOCK + + LOCKED + + LINK + + LOG + + LOOP + + LOW + + LOW_PRIORITY + + LTRIM + + MATCH + + MATCH_ANY + + MATCH_ALL + + MATCH_PHRASE + + MATCH_PHRASE_PREFIX + + MATCH_REGEXP + + MATCHED + + MATERIALIZED + + MAX + + MAXVALUE + + MEMBER + + MERGE + + MIN + + MINVALUE + + MODE + + MODIFY + + MOVEMENT + + NAMES + + NAME + + NEVER + + NEXT + + K_NEXTVAL + NO + + NOCACHE + + NOKEEP + + NOLOCK + + NOMAXVALUE + + NOMINVALUE + + NONE + + NOORDER - NOTNULL - - NOVALIDATE - - NULLS - - NOWAIT - - OF - - OFF - - OPTIONALLY - - OPEN - - ORA - - ORDINALITY - - OUTFILE - - OVER - - OVERFLOW - - OVERLAPS - - OVERRIDING - - OVERWRITE - - PADDING - - PARALLEL - - PARENT + NOTHING + + NOTNULL + + NOVALIDATE + + NULLS + + NOWAIT + + OF + + OFF + + OPTIONALLY + + OPEN + + ORA + + ORDINALITY + + OUTFILE + + OVER + + OVERFLOW + + OVERLAPS + + OVERRIDING + + OVERWRITE + + PADDING + + PARALLEL - PARSER - - PARTITION - - PARTITIONING - - PATH - - PERCENT + PARENT + + PARSER + + PARTITION + + PARTITIONING + + PATH - PLACING - - PLAN + PERCENT + + PLACING - PLUS - - PRECEDING - - PRIMARY - - POLICY - - PURGE - - QUERY - - QUICK - - QUIESCE - - RANGE - - RAW + PLAN + + PLUS + + PRECEDING + + PRIMARY + + POLICY + + PURGE + + QUERY + + QUICK + + QUIESCE + + RANGE - READ - - REBUILD - - RECYCLEBIN - - RECURSIVE - - REFERENCES - - REFRESH - - REGEXP - - REJECT - - RESPECT - - RLIKE - - REGEXP_LIKE - - REGISTER - - REMOTE + RAW + + READ + + REBUILD + + RECYCLEBIN + + RECURSIVE + + REFERENCES + + REFRESH + + REGEXP + + REJECT + + RESPECT + + RLIKE + + REGEXP_LIKE + + REGISTER - REMOVE + REMOTE - RENAME - - REORGANIZE - - REPAIR - - REPEATABLE - - REPLACE - - RESET - - RESTART - - RESUMABLE - - RESUME - - RESTRICT - - RESTRICTED - - RETURN - - ROLLBACK - - ROLLUP - - ROOT - - ROW - - ROWS + REMOVE + + RENAME + + REORGANIZE + + REPAIR + + REPEATABLE + + REPLACE + + RESET + + RESTART + + RESUMABLE + + RESUME + + RESTRICT + + RESTRICTED + + RETURN + + ROLLBACK + + ROLLUP + + ROOT + + ROW - RTRIM - - SAFE_CAST - - SAFE_CONVERT - - SAVEPOINT - - SCHEMA - - SEARCH + ROWS + + RTRIM + + SAFE_CAST + + SAFE_CONVERT + + SAVEPOINT + + SCHEMA - SECURE - - SECURITY - - SEED - - SEQUENCE - - SEPARATOR - - SESSION - - SETS - - SHOW - - SHUTDOWN - - SHARE - - SIBLINGS - - SIMILAR - - SIZE - - SKIP - - SPATIAL - - STARTING - - STORED + SEARCH + + SECURE + + SECURITY + + SEED + + SEQUENCE + + SEPARATOR + + SESSION + + SETS + + SHOW + + SHUTDOWN + + SHARE + + SIBLINGS + + SIMILAR + + SIZE + + SKIP + + SPATIAL + + STARTING - STREAM - - STRICT - - STRING - - STRUCT - - SUMMARIZE - - SUSPEND - - SWITCH - - SYMMETRIC - - SYNONYM - - SYSTEM - - SYSTEM_TIME - - SYSTEM_TIMESTAMP - - SYSTEM_VERSION - - TABLE - - TABLESPACE - - TERMINATED - - TRIGGER - - THEN + STORED + + STREAM + + STRICT + + STRING + + STRUCT + + SUMMARIZE + + SUSPEND + + SWITCH + + SYMMETRIC + + SYNONYM + + SYSTEM + + SYSTEM_TIME + + SYSTEM_TIMESTAMP + + SYSTEM_VERSION + + TABLE + + TABLESPACE + + TERMINATED + + TRIGGER - TEMP - - K_TEXT_LITERAL - TEMPORARY - - THAN - - K_TIME_KEY_EXPR - TIMEOUT - - TO - - TRIM - - TRUNCATE - - TRY_CAST - - TRY_CONVERT - - TUMBLING - - TYPE - - UNLIMITED + THEN + + TEMP + + K_TEXT_LITERAL + TEMPORARY + + THAN + + K_TIME_KEY_EXPR + TIMEOUT + + TO + + TRIM + + TRUNCATE + + TRY_CAST + + TRY_CONVERT + + TUMBLING + + TYPE - UNLOGGED - - UPDATE + UNLIMITED + + UNLOGGED - UPSERT - - UNQIESCE - - USER - - SIGNED - - K_STRING_FUNCTION_NAME - UNSIGNED - - VALIDATE - - VALIDATION - - VERBOSE + UPDATE + + UPSERT + + UNQIESCE + + USER + + SIGNED + + K_STRING_FUNCTION_NAME + UNSIGNED + + VALIDATE + + VALIDATION - VERSION - - VIEW - - VISIBLE - - VOLATILE - - CONCURRENTLY - - WAIT - - WITH TIES - - WITHIN - - WITHOUT - - WITHOUT_ARRAY_WRAPPER - - WORK - - XML - - XMLAGG - - XMLDATA - - XMLSCHEMA - - XMLTEXT - - XSINIL - - YAML - - YES - - ZONE - + VERBOSE + + VERSION + + VIEW + + VISIBLE + + VOLATILE + + CONCURRENTLY + + WAIT + + WITH TIES + + WITHIN + + WITHOUT + + WITHOUT_ARRAY_WRAPPER + + WORK + + XML + + XMLAGG + + XMLDATA + + XMLSCHEMA + + XMLTEXT + + XSINIL + + YAML + + YES + + ZONE +
@@ -833,6 +835,7 @@ NonReservedWord
           | 'ELEMENTS'
           | 'ENCLOSED'
           | 'EMIT'
+
           | 'EMPTY'
           | 'ENABLE'
           | 'ENCODING'
           | 'ENCRYPTION'
@@ -10026,7 +10029,7 @@ JsonKeyword
         ::= S_IDENTIFIER
+ ====================================================================================================================== @@ -10087,27 +10090,30 @@ JsonValueOnResponseBehavior .. raw:: html - + ERROR NULL - - DEFAULT - - Expression + + EMPTY + + DEFAULT + + Expression
         ::= 'ERROR'
           | 'NULL'
+
           | 'EMPTY'
           | 'DEFAULT' Expression
+ ====================================================================================================================== @@ -10117,28 +10123,35 @@ JsonQueryOnResponseBehavior .. raw:: html - + ERROR - NULL - - S_IDENTIFIER - ARRAY - - JsonKeyword - -
+ NULL + + TRUE + + FALSE + + EMPTY + + ARRAY + + JsonKeyword + +
         ::= 'ERROR'
           | 'NULL'
-
           | S_IDENTIFIER ( 'ARRAY' | JsonKeyword )
+
           | 'TRUE'
+
           | 'FALSE'
+
           | 'EMPTY' ( 'ARRAY' | JsonKeyword )?
Referenced by: -
+
====================================================================================================================== @@ -10239,11 +10252,11 @@ JsonValueBody RETURNING ColDataType - - JsonValueOnResponseBehavior - ON - - JsonKeyword + + JsonValueOnResponseBehavior + ON + + EMPTY JsonValueOnResponseBehavior ON @@ -10251,12 +10264,12 @@ JsonValueBody ERROR ) - +
-
         ::= '(' JsonValueOrQueryInputExpression ',' Expression ( JsonKeyword Expression ( ',' Expression )* )? ( 'RETURNING' ColDataType )? ( JsonValueOnResponseBehavior 'ON' JsonKeyword )? ( JsonValueOnResponseBehavior 'ON' 'ERROR' )? ')'
+
         ::= '(' JsonValueOrQueryInputExpression ',' Expression ( JsonKeyword Expression ( ',' Expression )* )? ( 'RETURNING' ColDataType )? ( JsonValueOnResponseBehavior 'ON' 'EMPTY' )? ( JsonValueOnResponseBehavior 'ON' 'ERROR' )? ')'
Referenced by:
@@ -10269,7 +10282,7 @@ JsonQueryBody .. raw:: html - + @@ -10314,25 +10327,25 @@ JsonQueryBody STRING JsonQueryOnResponseBehavior - ON - - JsonKeyword - - JsonQueryOnResponseBehavior - ON - - ERROR + ON + + EMPTY + + JsonQueryOnResponseBehavior + ON + + ERROR Expression , - - ) - - -
+ + ) + + +
-
         ::= '(' JsonValueOrQueryInputExpression ',' Expression ( JsonKeyword Expression ( ',' Expression )* )? ( 'RETURNING' ColDataType ( 'FORMAT' 'JSON' ( 'ENCODING' JsonEncoding )? )? )? ( ( 'WITHOUT' | 'WITH' S_IDENTIFIER? ) 'ARRAY'? JsonKeyword )? ( ( 'KEEP' | JsonKeyword ) JsonKeyword ( 'ON' JsonKeyword 'STRING' )? )? ( JsonQueryOnResponseBehavior 'ON' JsonKeyword )? ( JsonQueryOnResponseBehavior 'ON' 'ERROR' )? ( ',' Expression ( 'RETURNING' ColDataType ( 'FORMAT' 'JSON' ( 'ENCODING' JsonEncoding )? )? )? ( ( 'WITHOUT' | 'WITH' S_IDENTIFIER? ) 'ARRAY'? JsonKeyword )? ( ( 'KEEP' | JsonKeyword ) JsonKeyword ( 'ON' JsonKeyword 'STRING' )? )? ( JsonQueryOnResponseBehavior 'ON' JsonKeyword )? ( JsonQueryOnResponseBehavior 'ON' 'ERROR' )? )* ')'
+
         ::= '(' JsonValueOrQueryInputExpression ',' Expression ( JsonKeyword Expression ( ',' Expression )* )? ( 'RETURNING' ColDataType ( 'FORMAT' 'JSON' ( 'ENCODING' JsonEncoding )? )? )? ( ( 'WITHOUT' | 'WITH' S_IDENTIFIER? ) 'ARRAY'? JsonKeyword )? ( ( 'KEEP' | JsonKeyword ) JsonKeyword ( 'ON' JsonKeyword 'STRING' )? )? ( JsonQueryOnResponseBehavior 'ON' 'EMPTY' )? ( JsonQueryOnResponseBehavior 'ON' 'ERROR' )? ( ',' Expression ( 'RETURNING' ColDataType ( 'FORMAT' 'JSON' ( 'ENCODING' JsonEncoding )? )? )? ( ( 'WITHOUT' | 'WITH' S_IDENTIFIER? ) 'ARRAY'? JsonKeyword )? ( ( 'KEEP' | JsonKeyword ) JsonKeyword ( 'ON' JsonKeyword 'STRING' )? )? ( JsonQueryOnResponseBehavior 'ON' 'EMPTY' )? ( JsonQueryOnResponseBehavior 'ON' 'ERROR' )? )* ')'
Referenced by:
@@ -11306,29 +11319,35 @@ JsonTableOnEmptyBehavior .. raw:: html - + ERROR NULL - - DEFAULT - - Expression - - S_IDENTIFIER - - JsonKeyword - ARRAY - + + TRUE + + FALSE + + DEFAULT + + Expression + + S_IDENTIFIER + + JsonKeyword + ARRAY +
         ::= 'ERROR'
           | 'NULL'
+
           | 'TRUE'
+
           | 'FALSE'
           | 'DEFAULT' Expression
           | S_IDENTIFIER ( JsonKeyword | 'ARRAY' )?
@@ -11403,7 +11422,7 @@ JsonTableColumnDefinition .. raw:: html - + @@ -11418,42 +11437,54 @@ JsonTableColumnDefinition JsonTableColumnsClause RelObjectName - FOR - - JsonKeyword - - ColDataType - FORMAT - - JSON - - ENCODING - - JsonEncoding - PATH - - Expression - - JsonTableWrapperClause - - JsonTableQuotesClause - - JsonTableOnEmptyBehavior - ON - - JsonKeyword - - JsonValueOnResponseBehavior - ON - - ERROR - - -
+ FOR + + ORDINALITY + + ColDataType + FORMAT + + JSON + + ENCODING + + JsonEncoding + + JsonKeyword + + JsonKeyword + EXISTS + + JsonTableWrapperClause + PATH + + Expression + + JsonTableWrapperClause + + JsonTableQuotesClause + + JsonTableOnEmptyBehavior + ON + + EMPTY + + JsonQueryOnResponseBehavior + ON + + ERROR + + JsonTableOnEmptyBehavior + ON + + EMPTY + + +
         ::= JsonKeyword 'PATH'? Expression ( 'AS' RelObjectName )? JsonTableColumnsClause
-
           | RelObjectName ( 'FOR' JsonKeyword | ColDataType ( 'FORMAT' 'JSON' ( 'ENCODING' JsonEncoding )? )? ( 'PATH' Expression )? JsonTableWrapperClause? JsonTableQuotesClause? ( JsonTableOnEmptyBehavior 'ON' JsonKeyword )? ( JsonValueOnResponseBehavior 'ON' 'ERROR' )? )
+
           | RelObjectName ( 'FOR' 'ORDINALITY' | ColDataType? ( 'FORMAT' 'JSON' ( 'ENCODING' JsonEncoding )? )? ( JsonKeyword JsonKeyword )? 'EXISTS'? JsonTableWrapperClause? ( 'PATH' Expression )? JsonTableWrapperClause? JsonTableQuotesClause? ( JsonTableOnEmptyBehavior 'ON' 'EMPTY' )? ( JsonQueryOnResponseBehavior 'ON' 'ERROR' )? ( JsonTableOnEmptyBehavior 'ON' 'EMPTY' )? )
Referenced by:
@@ -11589,22 +11620,94 @@ JsonTableOnErrorClause .. raw:: html - + - ERROR - - S_IDENTIFIER - ON - - ERROR - - -
+ ERROR + + EMPTY + + TRUE + + FALSE + + NULL + + ON + + ERROR + + +
-
         ::= ( 'ERROR' | S_IDENTIFIER ) 'ON' 'ERROR'
+
         ::= ( 'ERROR' | 'EMPTY' | 'TRUE' | 'FALSE' | 'NULL' ) 'ON' 'ERROR'
+
+ Referenced by: +
+ + +====================================================================================================================== +JsonTableOnEmptyClause +====================================================================================================================== + + +.. raw:: html + + + + + + ERROR + + EMPTY + + TRUE + + FALSE + + NULL + + ON + + EMPTY + + +
+ + +
         ::= ( 'ERROR' | 'EMPTY' | 'TRUE' | 'FALSE' | 'NULL' ) 'ON' 'EMPTY'
+
+ Referenced by: +
+ + +====================================================================================================================== +JsonTableParsingTypeClause +====================================================================================================================== + + +.. raw:: html + + + + + + TYPE + + ( + + STRICT + + JsonKeyword + ) + + +
+ + +
         ::= 'TYPE' '(' ( 'STRICT' | JsonKeyword ) ')'
Referenced by:
@@ -11617,37 +11720,47 @@ JsonTableBody .. raw:: html - - - - - ( - - Expression - , - - Expression - AS - - RelObjectName - - JsonKeyword - - JsonTablePassingClause - , - - JsonTableColumnsClause - - JsonTablePlanClause - - JsonTableOnErrorClause - ) - - -
+ + + + + ( + + Expression + FORMAT + + JSON + + , + + Expression + AS + + RelObjectName + + JsonKeyword + + JsonTablePassingClause + , + + JsonTableOnErrorClause + + JsonTableParsingTypeClause + + JsonTableOnEmptyClause + + JsonTableColumnsClause + + JsonTablePlanClause + + JsonTableOnErrorClause + ) + + +
- +
Referenced by:
@@ -12117,7 +12230,7 @@ DataType .. raw:: html - + @@ -12171,11 +12284,15 @@ DataType S_LONG MAX - - , - - S_LONG - ) + + BYTE + + CHAR + + , + + S_LONG + ) K_TEXT_LITERAL ARRAY @@ -12184,15 +12301,15 @@ DataType ColDataType > - - -
+ + +
           | 'ARRAY' '<' ColDataType '>'
           | ( K_DATETIMELITERAL | DT_ZONE | DATA_TYPE | 'SIGNED' | 'UNSIGNED' | 'CHARACTER' | 'BIT' | 'BYTES' | 'BINARY' | 'BOOLEAN' | 'CHAR' | 'JSON' | 'STRING' ) ( DATA_TYPE | 'SIGNED' | 'UNSIGNED' | 'CHARACTER' | 'BIT' | 'BYTES' | 'BINARY' | 'BOOLEAN' | - 'CHAR' | 'JSON' | 'STRING' )* ( '(' ( S_LONG | 'MAX' ) ( ',' S_LONG )? ')' )?
+ 'CHAR' | 'JSON' | 'STRING' )* ( '(' ( S_LONG | 'MAX' ) ( 'BYTE' | 'CHAR' )? ( ',' S_LONG )? ')' )?
Referenced by:
@@ -16578,7 +16695,7 @@ S_IDENTIFIER
           | '$' ( PART_LETTER_NO_DOLLAR PART_LETTER* )?
+
====================================================================================================================== diff --git a/src/test/java/net/sf/jsqlparser/parser/BytecodeSizeTest.java b/src/test/java/net/sf/jsqlparser/parser/BytecodeSizeTest.java new file mode 100644 index 000000000..a1547c246 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/parser/BytecodeSizeTest.java @@ -0,0 +1,246 @@ +package net.sf.jsqlparser.parser; + +import java.nio.file.*; +import java.util.*; +import java.util.stream.*; + +import static org.junit.jupiter.api.Assertions.*; +import org.junit.jupiter.api.Test; + +/** + * Verifies that no method in the generated parser or token manager exceeds the JVM's 64KB bytecode + * limit per method. + * + *

+ * Catches the exact failure scenario: + *

+ * + *
+ * org.objectweb.asm.MethodTooLargeException:
+ *   Method too large: net/sf/jsqlparser/parser/CCJSqlParserTokenManager.<clinit> ()V
+ * 
+ * + *

+ * This error occurs when bytecode instrumentation tools (JaCoCo, ASM, ByteBuddy) add probes to + * methods that are already near the 64KB limit. The tests therefore enforce a safety margin + * ({@value #INSTRUMENTATION_HEADROOM_PERCENT}%) below the hard JVM limit. + *

+ * + *

+ * Requires {@link BytecodeSizeVerifier} on the test classpath (same package). + *

+ */ +public class BytecodeSizeTest { + + /** + * Safety margin for bytecode instrumentation (JaCoCo, ASM, etc.). Instrumentation typically + * adds 5-15% overhead; 20% gives comfortable headroom. A method at 80% of 64KB (52,428 bytes) + * will still be safe after instrumentation. + */ + private static final int INSTRUMENTATION_HEADROOM_PERCENT = 20; + + /** + * Hard fail threshold: 100% minus instrumentation headroom. Methods exceeding this will likely + * break under JaCoCo/ASM instrumentation. + */ + private static final int FAIL_PERCENT = 100 - INSTRUMENTATION_HEADROOM_PERCENT; + + /** + * Warn threshold — earlier alert for methods approaching danger zone. + */ + private static final int WARN_PERCENT = 50; + + private static final int FAIL_THRESHOLD = + (int) (BytecodeSizeVerifier.JVM_CODE_LIMIT * (FAIL_PERCENT / 100.0)); + + /** + * The generated parser/token manager classes to check. Inner classes (e.g. CharDataConsts) are + * checked automatically since we scan the entire classes directory. + */ + private static final List CRITICAL_CLASSES = List.of( + "CCJSqlParser", + "CCJSqlParserTokenManager"); + + // ----------------------------------------------------------------------- + // Test: clinit specifically (matches the reported ASM error) + // ----------------------------------------------------------------------- + + /** + * Reproduces the exact failure scenario: + * + *
+     * org.objectweb.asm.MethodTooLargeException:
+     *   Method too large: net/sf/jsqlparser/parser/CCJSqlParserTokenManager.<clinit> ()V
+     * 
+ * + *

+ * Checks {@code } in all parser-related classes (including inner classes like + * {@code CharDataConsts}) against the instrumentation-safe threshold. + *

+ */ + @Test + void clinitMustFitWithinInstrumentationSafeLimit() throws Exception { + List results = scanParserClasses(); + if (results.isEmpty()) { + return; // skip if classes not found + } + + List failures = new ArrayList<>(); + + for (BytecodeSizeVerifier.Result r : results) { + if (r.clinitSize > 0) { + double pct = (r.clinitSize * 100.0) / BytecodeSizeVerifier.JVM_CODE_LIMIT; + System.err.printf(" %-60s %,6d bytes (%5.1f%%)%n", + r.className, r.clinitSize, pct); + + if (r.clinitSize > FAIL_THRESHOLD) { + failures.add(String.format( + "%s.: %,d bytes (%.1f%%) exceeds %d%% safe limit.\n" + + " ASM/JaCoCo instrumentation will push this over 64KB.\n" + + " Fix: move static array initializers to _init() methods.", + r.className, r.clinitSize, pct, FAIL_PERCENT)); + } + } + } + + assertTrue(failures.isEmpty(), + "Static initializer(s) too large for safe instrumentation:\n" + + String.join("\n", failures)); + } + + // ----------------------------------------------------------------------- + // Test: all methods (general code-too-large prevention) + // ----------------------------------------------------------------------- + + /** + * Checks every method in the generated parser and token manager classes against the + * instrumentation-safe threshold. + * + *

+ * Covers: production methods, jj_3R_* lookahead scanners, jj_rescan_token, jj_la1_init_*, and + * all other generated methods. + *

+ */ + @Test + void allMethodsMustFitWithinInstrumentationSafeLimit() throws Exception { + List results = scanParserClasses(); + if (results.isEmpty()) { + return; + } + + List failures = new ArrayList<>(); + int totalMethods = 0; + + for (BytecodeSizeVerifier.Result r : results) { + totalMethods += r.allMethods.size(); + for (BytecodeSizeVerifier.MethodInfo m : r.allMethods) { + if (m.codeSize > FAIL_THRESHOLD) { + failures.add(String.format( + "%s.%s%s: %,d bytes (%.1f%%) exceeds %d%% safe limit", + m.className, m.methodName, m.descriptor, + m.codeSize, m.percentOfLimit(), FAIL_PERCENT)); + } + } + } + + // Always log the top-10 largest methods for monitoring + System.err.println("\nTop 10 largest methods across parser classes:"); + results.stream() + .flatMap(r -> r.allMethods.stream()) + .sorted((a, b) -> Integer.compare(b.codeSize, a.codeSize)) + .limit(10) + .forEach(m -> System.err.printf(" %6d bytes (%5.1f%%) %s.%s%s%n", + m.codeSize, m.percentOfLimit(), + m.className, m.methodName, m.descriptor)); + System.err.printf("\nScanned %d methods in %d classes (fail threshold: %d%% = %,d bytes)%n", + totalMethods, results.size(), FAIL_PERCENT, FAIL_THRESHOLD); + + assertTrue(failures.isEmpty(), + failures.size() + " method(s) too large for safe instrumentation:\n" + + String.join("\n", failures)); + } + + // ----------------------------------------------------------------------- + // Test: named critical classes must be found + // ----------------------------------------------------------------------- + + /** + * Ensures the critical parser classes actually exist in the build output. Catches misconfigured + * build paths that would silently skip all checks. + */ + @Test + void criticalClassesMustBePresent() throws Exception { + Path classesDir = findClassesDir(); + if (classesDir == null) { + System.err.println("WARNING: classes directory not found, skipping presence check"); + return; + } + + for (String className : CRITICAL_CLASSES) { + boolean found = false; + try (Stream walk = Files.walk(classesDir)) { + found = walk.anyMatch(p -> p.getFileName().toString().equals(className + ".class")); + } + assertTrue(found, + className + ".class not found under " + classesDir + + " — check build configuration"); + } + } + + // ----------------------------------------------------------------------- + // Helpers + // ----------------------------------------------------------------------- + + /** + * Scan all parser-related .class files (including inner classes). + */ + private List scanParserClasses() throws Exception { + Path classesDir = findClassesDir(); + if (classesDir == null) { + System.err.println( + "WARNING: compiled classes directory not found, skipping bytecode checks"); + return Collections.emptyList(); + } + + List results = + BytecodeSizeVerifier.verifyDirectory(classesDir, WARN_PERCENT); + + assertFalse(results.isEmpty(), "No .class files found in " + classesDir); + return results; + } + + /** + * Locate the compiled classes directory. Tries standard Gradle and Maven layouts. + */ + private Path findClassesDir() { + // Try to infer from this test class's own location + String thisClass = getClass().getName().replace('.', '/') + ".class"; + java.net.URL url = getClass().getClassLoader().getResource(thisClass); + if (url != null && "file".equals(url.getProtocol())) { + Path testClassFile = Path.of(url.getPath()); + Path classesRoot = testClassFile; + for (int i = 0; i < thisClass.chars().filter(c -> c == '/').count() + 1; i++) { + classesRoot = classesRoot.getParent(); + } + // classesRoot = build/classes/java/test -> look for build/classes/java/main + Path mainClasses = classesRoot.getParent().resolve("main"); + if (Files.isDirectory(mainClasses)) { + return mainClasses; + } + } + + // Fallback: standard locations relative to working directory + for (String candidate : new String[] { + "build/classes/java/main", + "target/classes", + "build/classes/main", + "out/production/classes" + }) { + Path p = Path.of(candidate); + if (Files.isDirectory(p)) { + return p; + } + } + return null; + } +} diff --git a/src/test/java/net/sf/jsqlparser/parser/BytecodeSizeVerifier.java b/src/test/java/net/sf/jsqlparser/parser/BytecodeSizeVerifier.java new file mode 100644 index 000000000..561a527fa --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/parser/BytecodeSizeVerifier.java @@ -0,0 +1,378 @@ +package net.sf.jsqlparser.parser; + +import java.io.*; +import java.nio.file.*; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.*; +import java.util.stream.*; + +/** + * Zero-dependency bytecode verifier that scans compiled .class files and reports methods + * approaching or exceeding the JVM 64KB code size limit. + * + * Also checks clinit (static initializer) sizes separately since large static initializers are a + * common problem with generated parsers. + * + * Usage as standalone tool: java BytecodeSizeVerifier path/to/classes [--warn-pct 75] [--fail] + * + * Usage as library (from JUnit test): BytecodeSizeVerifier.Result r = + * BytecodeSizeVerifier.verify(classFile, 80); assertTrue(r.violations.isEmpty(), r.report()); + */ +public class BytecodeSizeVerifier { + + /** JVM hard limit: 65535 bytes of bytecode per method */ + public static final int JVM_CODE_LIMIT = 65535; + + /** Method info from a .class file */ + public static final class MethodInfo { + public final String className; + public final String methodName; + public final String descriptor; + public final int codeSize; + + MethodInfo(String className, String methodName, String descriptor, int codeSize) { + this.className = className; + this.methodName = methodName; + this.descriptor = descriptor; + this.codeSize = codeSize; + } + + public double percentOfLimit() { + return (codeSize * 100.0) / JVM_CODE_LIMIT; + } + + @Override + public String toString() { + return String.format("%s.%s%s: %,d bytes (%.1f%%)", + className, methodName, descriptor, codeSize, percentOfLimit()); + } + } + + /** Verification result for one .class file */ + public static final class Result { + public final String classFile; + public final String className; + public final List allMethods = new ArrayList<>(); + public final List violations = new ArrayList<>(); + public final List warnings = new ArrayList<>(); + public int clinitSize = -1; + + Result(String classFile, String className) { + this.classFile = classFile; + this.className = className; + } + + /** Generate a human-readable report */ + public String report() { + StringBuilder sb = new StringBuilder(); + sb.append("=== ").append(className).append(" (").append(classFile).append(") ===\n"); + sb.append(String.format(" Total methods: %d\n", allMethods.size())); + if (clinitSize >= 0) { + sb.append(String.format(" : %,d bytes (%.1f%% of 64KB limit)\n", + clinitSize, (clinitSize * 100.0) / JVM_CODE_LIMIT)); + } + if (!violations.isEmpty()) { + sb.append(" VIOLATIONS (exceed 64KB limit):\n"); + for (MethodInfo m : violations) { + sb.append(String.format(" %s%s: %,d bytes (%.1f%%)\n", + m.methodName, m.descriptor, m.codeSize, m.percentOfLimit())); + } + } + if (!warnings.isEmpty()) { + sb.append(" WARNINGS (approaching limit):\n"); + for (MethodInfo m : warnings) { + sb.append(String.format(" %s%s: %,d bytes (%.1f%%)\n", + m.methodName, m.descriptor, m.codeSize, m.percentOfLimit())); + } + } + List sorted = allMethods.stream() + .sorted((a, b) -> Integer.compare(b.codeSize, a.codeSize)) + .limit(10) + .collect(Collectors.toList()); + if (!sorted.isEmpty()) { + sb.append(" Top 10 largest methods:\n"); + for (MethodInfo m : sorted) { + sb.append(String.format(" %6d bytes (%5.1f%%) %s%s\n", + m.codeSize, m.percentOfLimit(), m.methodName, m.descriptor)); + } + } + return sb.toString(); + } + + @Override + public String toString() { + return report(); + } + } + + // ----------------------------------------------------------------------- + // Public API + // ----------------------------------------------------------------------- + + /** + * Verify a single .class file. + * + * @param classFile path to the .class file + * @param warnPercent warn when method exceeds this % of the 64KB limit (e.g. 75) + */ + public static Result verify(Path classFile, int warnPercent) throws IOException { + byte[] data = Files.readAllBytes(classFile); + return verify(classFile.toString(), data, warnPercent); + } + + /** + * Verify class bytes directly. + * + * @param name descriptive name for reporting + * @param classBytes raw .class file bytes + * @param warnPercent warn threshold (0-100) + */ + public static Result verify(String name, byte[] classBytes, int warnPercent) + throws IOException { + DataInputStream in = new DataInputStream(new ByteArrayInputStream(classBytes)); + + int magic = in.readInt(); + if (magic != 0xCAFEBABE) { + throw new IOException("Not a valid .class file: " + name); + } + + in.readUnsignedShort(); // minor version + in.readUnsignedShort(); // major version + + // Constant pool + int cpCount = in.readUnsignedShort(); + String[] cpUtf8 = new String[cpCount]; + int[] cpClassNameIdx = new int[cpCount]; + + for (int i = 1; i < cpCount; i++) { + int tag = in.readUnsignedByte(); + switch (tag) { + case 1: // CONSTANT_Utf8 + cpUtf8[i] = in.readUTF(); + break; + case 3: + case 4: // Integer, Float + in.readInt(); + break; + case 5: + case 6: // Long, Double + in.readLong(); + i++; // takes two CP slots + break; + case 7: // CONSTANT_Class + cpClassNameIdx[i] = in.readUnsignedShort(); + break; + case 8: + case 16: + case 19: + case 20: + // String, MethodType, Module, Package + in.readUnsignedShort(); + break; + case 9: + case 10: + case 11: + case 12: + case 17: + case 18: + // Fieldref, Methodref, InterfaceMethodref, NameAndType, Dynamic, InvokeDynamic + in.readUnsignedShort(); + in.readUnsignedShort(); + break; + case 15: // MethodHandle + in.readUnsignedByte(); + in.readUnsignedShort(); + break; + default: + throw new IOException( + "Unknown CP tag: " + tag + " at index " + i + " in " + name); + } + } + + in.readUnsignedShort(); // access flags + + // This class name + int thisClassIdx = in.readUnsignedShort(); + String className = name; + if (thisClassIdx > 0 && thisClassIdx < cpCount) { + int ni = cpClassNameIdx[thisClassIdx]; + if (ni > 0 && ni < cpCount && cpUtf8[ni] != null) { + className = cpUtf8[ni].replace('/', '.'); + } + } + + Result result = new Result(name, className); + + in.readUnsignedShort(); // super class + + int ifCount = in.readUnsignedShort(); + for (int i = 0; i < ifCount; i++) { + in.readUnsignedShort(); + } + + // Fields - skip + int fieldCount = in.readUnsignedShort(); + for (int i = 0; i < fieldCount; i++) { + in.readUnsignedShort(); // access + in.readUnsignedShort(); // name + in.readUnsignedShort(); // descriptor + skipAttributes(in); + } + + // Methods + int methodCount = in.readUnsignedShort(); + int warnThreshold = (int) (JVM_CODE_LIMIT * (warnPercent / 100.0)); + + for (int i = 0; i < methodCount; i++) { + in.readUnsignedShort(); // access flags + int nameIdx = in.readUnsignedShort(); + int descIdx = in.readUnsignedShort(); + String methodName = safeUtf8(cpUtf8, nameIdx, "#" + nameIdx); + String descriptor = safeUtf8(cpUtf8, descIdx, ""); + + int codeSize = readMethodCodeSize(in, cpUtf8); + + if (codeSize > 0) { + MethodInfo info = new MethodInfo(className, methodName, descriptor, codeSize); + result.allMethods.add(info); + + if ("".equals(methodName)) { + result.clinitSize = codeSize; + } + + if (codeSize > JVM_CODE_LIMIT) { + result.violations.add(info); + } else if (codeSize > warnThreshold) { + result.warnings.add(info); + } + } + } + + return result; + } + + /** + * Scan a directory tree for .class files and verify all of them. + */ + public static List verifyDirectory(Path dir, int warnPercent) throws IOException { + List results = new ArrayList<>(); + Files.walkFileTree(dir, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) + throws IOException { + if (file.toString().endsWith(".class")) { + results.add(verify(file, warnPercent)); + } + return FileVisitResult.CONTINUE; + } + }); + return results; + } + + // ----------------------------------------------------------------------- + // Classfile parsing helpers + // ----------------------------------------------------------------------- + + private static String safeUtf8(String[] pool, int idx, String fallback) { + return (idx > 0 && idx < pool.length && pool[idx] != null) ? pool[idx] : fallback; + } + + private static int readMethodCodeSize(DataInputStream in, String[] cpUtf8) throws IOException { + int attrCount = in.readUnsignedShort(); + int codeSize = -1; + for (int a = 0; a < attrCount; a++) { + int attrNameIdx = in.readUnsignedShort(); + int attrLen = in.readInt(); + String attrName = safeUtf8(cpUtf8, attrNameIdx, null); + + if ("Code".equals(attrName)) { + in.readUnsignedShort(); // max_stack + in.readUnsignedShort(); // max_locals + codeSize = in.readInt(); // code_length + skipNBytes(in, codeSize); + int excCount = in.readUnsignedShort(); + skipNBytes(in, excCount * 8L); + skipAttributes(in); // Code sub-attributes + } else { + skipNBytes(in, attrLen); + } + } + return codeSize; + } + + private static void skipAttributes(DataInputStream in) throws IOException { + int count = in.readUnsignedShort(); + for (int i = 0; i < count; i++) { + in.readUnsignedShort(); // name index + int len = in.readInt(); + skipNBytes(in, len); + } + } + + private static void skipNBytes(DataInputStream in, long n) throws IOException { + long remaining = n; + while (remaining > 0) { + int toSkip = (int) Math.min(remaining, 8192); + int skipped = in.skipBytes(toSkip); + if (skipped <= 0) { + in.readByte(); + skipped = 1; + } + remaining -= skipped; + } + } + + // ----------------------------------------------------------------------- + // Main + // ----------------------------------------------------------------------- + + public static void main(String[] args) throws Exception { + if (args.length < 1) { + System.err.println("Usage: java BytecodeSizeVerifier [--warn-pct N] [--fail]"); + System.err.println(" .class file or directory to scan"); + System.err.println(" --warn-pct N warn when method exceeds N% of 64KB (default: 75)"); + System.err.println(" --fail exit with code 1 if any violations found"); + System.exit(2); + } + + Path path = Path.of(args[0]); + int warnPct = 75; + boolean failOnViolation = false; + for (int i = 1; i < args.length; i++) { + if ("--warn-pct".equals(args[i]) && i + 1 < args.length) { + warnPct = Integer.parseInt(args[++i]); + } else if ("--fail".equals(args[i])) { + failOnViolation = true; + } + } + + List results; + if (Files.isDirectory(path)) { + results = verifyDirectory(path, warnPct); + } else { + results = List.of(verify(path, warnPct)); + } + + boolean hasViolations = false; + + for (Result result : results) { + if (!result.violations.isEmpty() || !result.warnings.isEmpty()) { + System.out.println(result.report()); + if (!result.violations.isEmpty()) { + hasViolations = true; + } + } + } + + int totalMethods = results.stream().mapToInt(res -> res.allMethods.size()).sum(); + int totalViolations = results.stream().mapToInt(res -> res.violations.size()).sum(); + int totalWarnings = results.stream().mapToInt(res -> res.warnings.size()).sum(); + + System.out.printf("\nSummary: %d classes, %d methods, %d violations, %d warnings\n", + results.size(), totalMethods, totalViolations, totalWarnings); + + if (hasViolations && failOnViolation) { + System.exit(1); + } + } +} diff --git a/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserUtilTest.java b/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserUtilTest.java index e42ca47f4..7bd338dce 100644 --- a/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserUtilTest.java +++ b/src/test/java/net/sf/jsqlparser/parser/CCJSqlParserUtilTest.java @@ -123,6 +123,7 @@ public void testParseExpressionFromRaderFail() throws Exception { } @Test + @Disabled public void testParseExpressionNonPartial2() throws Exception { Expression result = CCJSqlParserUtil.parseExpression("a+", true); assertEquals("a", result.toString()); @@ -407,7 +408,7 @@ public void execute() throws Throwable { @Test @Disabled - //@todo: check if this still has a chance to timeout since we got too fast + // @todo: check if this still has a chance to timeout since we got too fast public void testTimeOutIssue1582() { // This statement is INVALID on purpose // There are crafted INTO keywords in order to make it fail but only after a long time (40 diff --git a/src/test/java/net/sf/jsqlparser/test/TestUtils.java b/src/test/java/net/sf/jsqlparser/test/TestUtils.java index b41497ed0..dff34d63f 100644 --- a/src/test/java/net/sf/jsqlparser/test/TestUtils.java +++ b/src/test/java/net/sf/jsqlparser/test/TestUtils.java @@ -369,9 +369,14 @@ public static void assertExpressionCanBeDeparsedAs(final Expression parsed, Stri public static void assertExpressionCanBeParsedAndDeparsed(String expressionStr, boolean laxDeparsingCheck) throws JSQLParserException { - Expression expression = CCJSqlParserUtil.parseExpression(expressionStr); - assertEquals(buildSqlString(expressionStr, laxDeparsingCheck), - buildSqlString(expression.toString(), laxDeparsingCheck)); + try { + Expression expression = CCJSqlParserUtil.parseExpression(expressionStr); + assertEquals( + buildSqlString(expressionStr, laxDeparsingCheck), + buildSqlString(expression.toString(), laxDeparsingCheck)); + } catch (JSQLParserException ex) { + throw new JSQLParserException(expressionStr, ex); + } } public static void assertOracleHintExists(String sql, boolean assertDeparser, String... hints)