diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 89826d9e5..000000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,9 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "gradle" # Specify Gradle as the package manager - directory: "/" # Root directory of your project - schedule: - interval: "weekly" # Define how often Dependabot should check for updates - ignore: - - dependency-name: "se.bjurr.gitchangelog.git-changelog-gradle-plugin" - versions: ["*"] # This will ignore all versions for this specific plugin diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8cf7d4c46..a3108f35b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,63 +2,103 @@ name: CI Pipeline on: push: - branches: [ "master" ] + branches: [ "**" ] # Run on every commit to any branch pull_request: - branches: [ "master" ] + branches: [ "**" ] # Run for PRs from any branch workflow_dispatch: permissions: write-all jobs: gradle_check: - runs-on: ubuntu-latest + name: Gradle Check + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ ubuntu-latest, windows-latest, macos-latest ] steps: - uses: actions/checkout@main with: fetch-depth: 0 + - name: Set up JDK 17 uses: actions/setup-java@main with: java-version: '17' distribution: 'temurin' - - name: Build with Gradle + + - name: Set up Gradle uses: gradle/actions/setup-gradle@main + - name: Run Gradle Check run: ./gradlew check + maven_verify: + name: Maven Verify + needs: gradle_check # ✅ Run only after Gradle check succeeds + runs-on: ${{ matrix.os }} + strategy: + matrix: +# os: [ ubuntu-latest, windows-latest, macos-latest ] + os: [ ubuntu-latest, macos-latest ] + steps: + - uses: actions/checkout@main + with: + fetch-depth: 0 + + - name: Set up JDK 17 + uses: actions/setup-java@main + with: + java-version: '17' + distribution: 'temurin' + + - name: Run Maven Verify + run: mvn --batch-mode verify + gradle_publish: - needs: gradle_check + name: Gradle Publish + needs: [ gradle_check, maven_verify ] # ✅ Run only after both succeed + if: github.ref == 'refs/heads/master' && github.repository == 'JSQLParser/JSqlParser' # ✅ Only for master branch of main repo runs-on: ubuntu-latest steps: - uses: actions/checkout@main with: fetch-depth: 0 + - name: Set up JDK 17 uses: actions/setup-java@main with: java-version: '17' distribution: 'temurin' + - name: Build with Gradle uses: gradle/actions/setup-gradle@main + - name: Publish with Gradle run: ./gradlew publish env: ossrhUsername: ${{ secrets.OSSRHUSERNAME }} ossrhPassword: ${{ secrets.OSSRHPASSWORD }} + - uses: actions/setup-python@main + - name: Install XSLT Processor - run: sudo apt-get install xsltproc sphinx-common + run: sudo apt-get install -y xsltproc sphinx-common + - name: Install Python dependencies - #run: pip install furo myst_parser sphinx-prompt sphinx_substitution_extensions sphinx_issues sphinx_inline_tabs pygments - run: pip install furo myst_parser sphinx_substitution_extensions sphinx_issues sphinx_inline_tabs pygments + run: pip install manticore_sphinx_theme sphinx_javadoc_xml myst_parser sphinx_substitution_extensions sphinx_issues sphinx_inline_tabs pygments + - name: Build Sphinx documentation with Gradle - run: ./gradlew -DFLOATING_TOC=false gitChangelogTask renderRR xslt xmldoc sphinx - - name: Deploy Sphinx documentation + run: FLOATING_TOC=false ./gradlew -DFLOATING_TOC=false gitChangelogTask renderRR xslt xmldoc sphinx + + - name: Configure GitHub Pages uses: actions/configure-pages@main + - name: Upload artifact uses: actions/upload-pages-artifact@main with: path: 'build/sphinx' + - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@main diff --git a/.gitignore b/.gitignore old mode 100755 new mode 100644 index 7129aacf7..955e7bf2d --- a/.gitignore +++ b/.gitignore @@ -8,10 +8,10 @@ # Exclude the Auto-generated Changelog /src/site/sphinx/changelog.rst -/src/site/sphinx/javadoc_stable.rst /src/site/sphinx/syntax_stable.rst -/src/site/sphinx/javadoc_snapshot.rst /src/site/sphinx/syntax_snapshot.rst +/src/site/sphinx/javadoc_stable.xml +/src/site/sphinx/javadoc_snapshot.xml # Generated by javacc-maven-plugin /bin diff --git a/README.md b/README.md index 999f043e2..c310ef545 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,42 @@ -# [JSqlParser 5.3 Website](https://jsqlparser.github.io/JSqlParser) drawing +
+ + + + +
+

JSqlParser

+

Turn any SQL statement into a traversable tree of Java objects -- and back again.

+

An RDBMS-agnostic SQL parser for the JVM:
one grammar, twelve dialects, no native extensions.

+
+ +JSqlParser + +
[![CI](https://github.com/JSQLParser/JSqlParser/actions/workflows/ci.yml/badge.svg)](https://github.com/JSQLParser/JSqlParser/actions/workflows/ci.yml) [![Coverage Status](https://coveralls.io/repos/JSQLParser/JSqlParser/badge.svg?branch=master)](https://coveralls.io/r/JSQLParser/JSqlParser?branch=master) -[![Codacy Badge](https://app.codacy.com/project/badge/Grade/6f9a2d7eb98f45969749e101322634a1)](https://www.codacy.com/gh/JSQLParser/JSqlParser/dashboard?utm_source=github.com&utm_medium=referral&utm_content=JSQLParser/JSqlParser&utm_campaign=Badge_Grade) -[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.github.jsqlparser/jsqlparser/badge.svg)](http://maven-badges.herokuapp.com/maven-central/com.github.jsqlparser/jsqlparser) [![Javadocs](https://www.javadoc.io/badge/com.github.jsqlparser/jsqlparser.svg)](https://www.javadoc.io/doc/com.github.jsqlparser/jsqlparser) -[![Gitter](https://badges.gitter.im/JSQLParser/JSqlParser.svg)](https://gitter.im/JSQLParser/JSqlParser?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) +[![Codacy Badge](https://app.codacy.com/project/badge/Grade/6f9a2d7eb98f45969749e101322634a1)](https://www.codacy.com/gh/JSQLParser/JSqlParser/dashboard) +[![Manticore Build](https://img.shields.io/maven-central/v/com.manticore-projects.jsqlformatter/jsqlparser.svg?label=manticore%20build&color=ff420e)](https://central.sonatype.com/artifact/com.manticore-projects.jsqlformatter/jsqlparser) +[![Maven Central](https://img.shields.io/maven-central/v/com.github.jsqlparser/jsqlparser.svg?label=upstream%20release)](https://central.sonatype.com/artifact/com.github.jsqlparser/jsqlparser) +[![Javadocs](https://www.javadoc.io/badge/com.github.jsqlparser/jsqlparser.svg)](https://www.javadoc.io/doc/com.github.jsqlparser/jsqlparser) +[![GitHub Stars](https://img.shields.io/github/stars/JSQLParser/JSqlParser?style=flat&label=stars)](https://github.com/JSQLParser/JSqlParser/stargazers) +[![Gitter](https://badges.gitter.im/JSQLParser/JSqlParser.svg)](https://gitter.im/JSQLParser/JSqlParser) -A huge thank you to our sponsor, [Starlake.ai](https://starlake.ai/) who simplifies data ingestion, transformation, and orchestration, enabling faster delivery of high-quality data. Starlake has been instrumental in providing Piped SQL and numerous test cases for BigQuery, Redshift, DataBricks, and DuckDB. Show your support for ongoing development by visiting Starlake.ai and giving us a star! +**[Website](https://jsqlparser.github.io/JSqlParser)** · **[Samples](https://jsqlparser.github.io/JSqlParser/usage.html#parse-a-sql-statements)** · **[Syntax](https://jsqlparser.github.io/JSqlParser/syntax.html)** · **[Change Log](https://jsqlparser.github.io/JSqlParser/changelog.html#latest-changes-since-jsqlparser-version)** · **[Contributing](https://jsqlparser.github.io/JSqlParser/contribution.html)** -## Summary +
-Please visit our [WebSite](https://jsqlparser.github.io/JSqlParser) for detailed information. **JSqlParser** is a RDBMS agnostic SQL statement parser. It translates SQL statements into a traversable hierarchy of Java classes (see [Samples](https://jsqlparser.github.io/JSqlParser/usage.html#parse-a-sql-statements)): +--- + +## What it does + +Give it SQL. Get an AST you can walk, rewrite, and print back out. ```sql SELECT 1 FROM dual WHERE a = b +``` -/* produces the following AST - +```text SQL Text └─Statements: statement.select.PlainSelect ├─selectItems: statement.select.SelectItem @@ -25,7 +45,6 @@ SQL Text └─where: expression.operators.relational.EqualsTo ├─Column: a └─Column: b -*/ ``` ```java @@ -33,11 +52,8 @@ String sqlStr = "select 1 from dual where a=b"; PlainSelect select = (PlainSelect) CCJSqlParserUtil.parse(sqlStr); -SelectItem selectItem = - select.getSelectItems().get(0); -Assertions.assertEquals( - new LongValue(1) - , selectItem.getExpression()); +SelectItem selectItem = select.getSelectItems().get(0); +Assertions.assertEquals(new LongValue(1), selectItem.getExpression()); Table table = (Table) select.getFromItem(); Assertions.assertEquals("dual", table.getName()); @@ -48,73 +64,146 @@ Column b = (Column) equalsTo.getRightExpression(); Assertions.assertEquals("a", a.getColumnName()); Assertions.assertEquals("b", b.getColumnName()); ``` -## Support for `Piped SQL` -Work is progressing for parsing `Piped SQL`, a much saner and more logical way to write queries in its semantic order. -```sql -FROM Produce -|> WHERE - item != 'bananas' - AND category IN ('fruit', 'nut') -|> AGGREGATE COUNT(*) AS num_items, SUM(sales) AS total_sales - GROUP BY item -|> ORDER BY item DESC; +The tree is traversable with the Visitor pattern, and the same object model works in reverse: +build statements from Java with a [fluent API](https://jsqlparser.github.io/JSqlParser/usage.html#build-a-sql-statements) +and render them as SQL text. + +## Install + +Use the stable **Manticore builds**. They are released continuously from the current development +line and carry all of the performance and grammar work described below. The upstream +`com.github.jsqlparser` release on Maven Central is considerably older. + +```xml + + com.manticore-projects.jsqlformatter + jsqlparser + [5.3.218,) + ``` -For details, please see https://storage.googleapis.com/gweb-research2023-media/pubtools/1004848.pdf, https://cloud.google.com/bigquery/docs/reference/standard-sql/pipe-syntax and https://duckdb.org/docs/sql/query_syntax/from.html#from-first-syntax +```gradle +implementation("com.manticore-projects.jsqlformatter:jsqlparser:+") +``` -## Java Version +
+Upstream release and snapshots -JSQLParser-4.9 was the last JDK8 compatible version. JSQLParser-5.0 and later depend on JDK11 and introduce API breaking changes to the AST Visitors. Please see the Migration Guide for the details. +```xml + + com.github.jsqlparser + jsqlparser + 5.3 + +``` -Building JSQLParser-5.1 and newer with Gradle will depend on a JDK17 toolchain due to the used plugins. +Snapshot coordinates and repository setup are on the +[build dependencies page](https://jsqlparser.github.io/JSqlParser/usage.html#build-dependencies). -JSQLParser-5.4 Snapshot and later use JavaCC-8 Snapshots for generating the parser. +
## Performance -Unfortunately the released JSQLParser-5.2 shows a performance deterioration caused by commit [30cf5d7](https://github.com/JSQLParser/JSqlParser/commit/30cf5d7b930ae0a076f49deb5cc841d39e62b3dc) related to `FunctionAllColumns()`. -This has been resolved in JSQLParser 5.3-SNAPSHOT and JMH benchmarks have been added to avoid such regressions in the future. Further all `LOOKAHEAD` have been revised one by one, and we have gained back a very good performance of the Parser. +**11× faster than 5.3**, and the fastest parser on real-world SQL of any of the parsers +tested, in any language — 19× ahead of `sqlglot[c]` on JSqlParser's own `SELECT` test suite. + +
+ + SQL parser benchmark score + +
```text Benchmark (version) Mode Cnt Score Error Units -JSQLParserBenchmark.parseSQLStatements latest avgt 15 82.695 ± 2.841 ms/op +JSQLParserBenchmark.parseSQLStatements latest avgt 15 7.602 ± 0.135 ms/op JSQLParserBenchmark.parseSQLStatements 5.3 avgt 15 84.687 ± 3.321 ms/op -JSQLParserBenchmark.parseSQLStatements 5.1 avgt 15 86.592 ± 5.781 ms/op ``` -## [Supported Grammar and Syntax](https://jsqlparser.github.io/JSqlParser/syntax.html) +Methodology and the full cross-parser comparison against SQLGlot, `sqlglot[c]` and +polyglot-sql: **[jsqlparser-bench](https://github.com/manticore-projects/jsqlparser-bench)**. + +## What it parses + +JSqlParser targets the SQL standard plus all major RDBMS. One grammar covers all of them, +and missing syntax gets added on demand — [open an issue](https://github.com/JSQLParser/JSqlParser/issues). + +
+ +`BigQuery` · `Snowflake` · `DuckDB` · `Redshift` · `Oracle` · `MS SQL Server` · `Sybase` +`PostgreSQL` · `MySQL` · `MariaDB` · `DB2` · `H2` · `HSQLDB` · `Derby` · `SQLite` + +
+ +| | Statements | +|---|---| +| **Queries** | `SELECT` · `WITH …` · Piped SQL | +| **DML** | `INSERT` · `UPDATE` · `UPSERT` · `MERGE` · `DELETE` · `TRUNCATE TABLE` | +| **DDL** | `CREATE …` · `ALTER …` · `DROP …` | +| **PostgreSQL RLS** | `CREATE POLICY` · `ALTER TABLE … ENABLE`/`DISABLE`/`FORCE`/`NO FORCE ROW LEVEL SECURITY` | +| **Salesforce SOQL** | `INCLUDES` · `EXCLUDES` | + +Beyond statement shapes, the grammar handles nested sub-selects, bind parameters (`?`, +`:name`), window and analytic functions, Oracle hints, and the T-SQL square-bracket versus +array-literal ambiguity. The complete reference is on the +[syntax page](https://jsqlparser.github.io/JSqlParser/syntax.html). + +## Piped SQL + +Support is progressing for Piped SQL, which writes queries in the order they actually +execute rather than the order SQL historically demanded. + +```sql +FROM Produce +|> WHERE + item != 'bananas' + AND category IN ('fruit', 'nut') +|> AGGREGATE COUNT(*) AS num_items, SUM(sales) AS total_sales + GROUP BY item +|> ORDER BY item DESC; +``` + +Background reading: the [Google research paper](https://storage.googleapis.com/gweb-research2023-media/pubtools/1004848.pdf), +[BigQuery pipe syntax](https://cloud.google.com/bigquery/docs/reference/standard-sql/pipe-syntax) +and [DuckDB FROM-first syntax](https://duckdb.org/docs/sql/query_syntax/from.html#from-first-syntax). -**JSqlParser** aims to support the SQL standard as well as all major RDBMS. Any missing syntax or features can be added on demand. +## Java version -| RDBMS | Statements | -|-----------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------| -| Oracle
MS SQL Server and Sybase
Postgres
MySQL and MariaDB
DB2
H2 and HSQLDB and Derby
SQLite | `SELECT`
`INSERT`, `UPDATE`, `UPSERT`, `MERGE`
`DELETE`, `TRUNCATE TABLE`
`CREATE ...`, `ALTER ....`, `DROP ...`
`WITH ...` | -| Salesforce SOQL | `INCLUDES`, `EXCLUDES` | -| Piped SQL (also known as FROM SQL) | | +| JSqlParser | Runtime | Notes | +|------------|---------|-------| +| 4.9 | JDK 8 | last JDK 8 compatible release | +| 5.0 and later | JDK 11 | breaking changes to the AST Visitors, see the Migration Guide | +| 5.1 and later | JDK 11 | building requires a **JDK 17 toolchain** (plugin requirement) | +| 5.4 and later | JDK 11 | parser generated with **JavaCC 8** | -**JSqlParser** can also be used to create SQL Statements from Java Code with a fluent API (see [Samples](https://jsqlparser.github.io/JSqlParser/usage.html#build-a-sql-statements)). +## Sister projects -## Sister Projects +- **[JSQLFormatter](https://manticore-projects.com/JSQLFormatter/index.html)** — pretty-printing and formatting of SQL text +- **[JSQLTranspiler](https://manticore-projects.com/JSQLTranspiler/index.html)** — dialect-specific rewriting, column resolution and lineage, by [Starlake.ai](https://starlake.ai/) -If you like JSqlParser then please check out its related projects: +## Alternatives -* [JSQLFormatter](https://manticore-projects.com/JSQLFormatter/index.html) for pretty printing and formatting SQL Text +The dual-licensed [JOOQ](https://www.jooq.org/doc/latest/manual/sql-building/sql-parser/) +ships a hand-written parser with broad RDBMS support, cross-dialect translation, SQL +transformation, and a JDBC proxy mode. Worth a look if translation between dialects is your +primary need rather than AST access. -* [JSQLTranspiler](https://manticore-projects.com/JSQLTranspiler/index.html) for dialect specific rewriting, SQL Column resolution and Lineage, provided by [Starlake.ai](https://starlake.ai/) +## Sponsor -## Alternatives to JSqlParser? -[**General SQL Parser**](http://www.sqlparser.com/features/introduce.php?utm_source=github-jsqlparser&utm_medium=text-general) looks pretty good, with extended SQL syntax (like PL/SQL and T-SQL) and java + .NET APIs. The tool is commercial (license available online), with a free download option. +A huge thank you to **[Starlake.ai](https://starlake.ai/)**, who simplify data ingestion, +transformation and orchestration for faster delivery of high-quality data. Starlake has been +instrumental in providing Piped SQL support and a large number of test cases for BigQuery, +Redshift, Databricks and DuckDB. If JSqlParser is useful to you, visit +[Starlake.ai](https://starlake.ai/) and give them a star. -Alternatively the dual-licensed [JOOQ](https://www.jooq.org/doc/latest/manual/sql-building/sql-parser/) provides a handwritten Parser supporting a lot of RDBMS, translation between dialects, SQL transformation, can be used as a JDBC proxy for translation and transformation purposes. +## Documentation -## [Documentation](https://jsqlparser.github.io/JSqlParser) - 1. [Samples](https://jsqlparser.github.io/JSqlParser/usage.html#parse-a-sql-statements) - 2. [Build Instructions](https://jsqlparser.github.io/JSqlParser/usage.html) and [Maven Artifact](https://jsqlparser.github.io/JSqlParser/usage.html#build-dependencies) - 3. [Contribution](https://jsqlparser.github.io/JSqlParser/contribution.html) - 4. [Change Log](https://jsqlparser.github.io/JSqlParser/changelog.html#latest-changes-since-jsqlparser-version) - 5. [Issues](https://github.com/JSQLParser/JSqlParser/issues) +1. [Samples](https://jsqlparser.github.io/JSqlParser/usage.html#parse-a-sql-statements) +2. [Build instructions](https://jsqlparser.github.io/JSqlParser/usage.html) and [Maven artifact](https://jsqlparser.github.io/JSqlParser/usage.html#build-dependencies) +3. [Contribution guide](https://jsqlparser.github.io/JSqlParser/contribution.html) +4. [Change log](https://jsqlparser.github.io/JSqlParser/changelog.html#latest-changes-since-jsqlparser-version) +5. [Issues](https://github.com/JSQLParser/JSqlParser/issues) ## License -**JSqlParser** is dual licensed under **LGPL V2.1** or **Apache Software License, Version 2.0**. +Dual licensed under **LGPL 2.1** or the **Apache License, Version 2.0**. Take your pick. diff --git a/build.gradle b/build.gradle index 843119600..319cbdcbb 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,8 @@ import se.bjurr.gitchangelog.plugin.gradle.GitChangelogTask import com.nwalsh.gradle.saxon.SaxonXsltTask +import java.time.Instant + buildscript { dependencies { classpath group: 'net.sf.saxon', name: 'Saxon-HE', version: 'latest.release' @@ -9,6 +11,7 @@ buildscript { plugins { id 'java' + id "com.vanniktech.maven.publish" version "latest.release" id 'maven-publish' id 'signing' @@ -60,9 +63,10 @@ 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 } @@ -72,15 +76,62 @@ version = getVersion( !System.getenv("RELEASE") ) group = 'com.github.jsqlparser' description = 'JSQLParser library' -repositories { - gradlePluginPortal() - mavenCentral() +tasks.register('generateBuildInfo') { + outputs.dir layout.buildDirectory.file("resources/main") + doLast { + def outputDir = new File( layout.buildDirectory.file("generated/sources/buildinfo/java/main").get().asFile, "net/sf/jsqlparser") + outputDir.mkdirs() + + def gitVersionStr = providers.exec { + commandLine "git", "--no-pager", "-C", project.projectDir, "describe", "--tags", "--always", "--dirty=-SNAPSHOT" + }.standardOutput.asText.get().trim() + + def gitCommitStr = providers.exec { + commandLine "git", "--no-pager", "-C", project.projectDir, "rev-parse", "--short", "HEAD" + }.standardOutput.asText.get().trim() + + def buildTime = Instant.now().toString() + + def content = """\ + |package net.sf.jsqlparser; + | + |public final class BuildInfo { + | public static final String NAME = "${project.name}"; + | public static final String VERSION = "${gitVersionStr}"; + | public static final String GIT_COMMIT = "${gitCommitStr ?: 'unknown'}"; + | public static final String BUILD_TIME = "${buildTime}"; + |} + """.stripMargin() + + new File(outputDir, "BuildInfo.java").text = content + } +} - // JavaCC 8 Snapshots - maven { - url = uri('https://central.sonatype.com/repository/maven-snapshots/') +// Make sure the file is included in the compiled sources +sourceSets { + main { + java { + srcDir layout.buildDirectory.file("generated/sources/buildinfo/java/main").get().asFile + } } +} + +tasks.withType(JavaCompile).configureEach { + mustRunAfter("generateBuildInfo") +} +tasks.withType(Pmd).configureEach { + mustRunAfter("generateBuildInfo") +} + +tasks.withType(Checkstyle).configureEach { + exclude '**/module-info.java', '**/package-info.java' + + mustRunAfter("generateBuildInfo") +} + +repositories { + mavenCentral() maven { url "https://dev.saxonica.com/maven" } } @@ -98,12 +149,13 @@ dependencies { testImplementation 'com.h2database:h2:+' // for JaCoCo Reports - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.11.4' - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.11.4' - testImplementation 'org.junit.jupiter:junit-jupiter-params:5.11.4' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.+' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.+' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.+' + testImplementation 'org.junit.jupiter:junit-jupiter-params:5+' // https://mvnrepository.com/artifact/org.mockito/mockito-junit-jupiter - testImplementation 'org.mockito:mockito-junit-jupiter:5.18.0' + testImplementation 'org.mockito:mockito-junit-jupiter:5.+' // Performance Benchmark testImplementation 'org.openjdk.jmh:jmh-core:+' @@ -113,30 +165,30 @@ 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 } - - 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 } -} -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' - } - } + testImplementation('com.manticore-projects.jsqlformatter:javacc-core:+') + testImplementation('com.manticore-projects.jsqlformatter:javacc-java:+') + + jmh 'org.openjdk.jmh:jmh-core:+' + jmh 'org.openjdk.jmh:jmh-generator-annprocess:+' + javacc('com.manticore-projects.jsqlformatter:javacc-core:+') + javacc('com.manticore-projects.jsqlformatter:javacc-java:+') } compileJavacc { - arguments = [grammar_encoding: 'UTF-8', static: 'false', java_template_type: 'modern'] + arguments = [ + grammar_encoding: 'UTF-8', + static: 'false', + java_template_type: 'modern', + // Comment this in to build the parser with tracing. + DEBUG_PARSER: 'false', + DEBUG_LOOKAHEAD: 'false', + LEGACY_EXCEPTION_HANDLING: 'false' + ] } java { withSourcesJar() - withJavadocJar() + // withJavadocJar() sourceCompatibility = '11' targetCompatibility = '11' @@ -158,20 +210,22 @@ javadoc { jar { manifest { attributes ( - "Automatic-Module-Name": "net.sf.jsqlparser" + "Automatic-Module-Name": "net.sf.jsqlparser" ) } bundle { properties.empty() bnd( - "Created-By": System.properties.get('user.name'), - "Bundle-SymbolicName": "net.sf.jsqlparser", - "Import-Package": "*", - "Export-Package": "net.sf.jsqlparser.*", - "Automatic-Module-Name": "net.sf.jsqlparser" + "Created-By": System.properties.get('user.name'), + "Bundle-SymbolicName": "net.sf.jsqlparser", + "Import-Package": "*", + "Export-Package": "net.sf.jsqlparser.*", + "Automatic-Module-Name": "net.sf.jsqlparser" ) } + + dependsOn(generateBuildInfo) } tasks.register('xmldoc', Javadoc) { @@ -183,12 +237,6 @@ tasks.register('xmldoc', Javadoc) { : "xmlDoclet/javadoc_stable.xml" ) - def rstFile = reporting.file( - version.endsWith("-SNAPSHOT") - ? "xmlDoclet/javadoc_snapshot.rst" - : "xmlDoclet/javadoc_stable.rst" - ) - source = sourceSets.main.allJava // add any generated Java sources source += fileTree(layout.buildDirectory.dir("generated/javacc").get().asFile) { @@ -205,16 +253,12 @@ tasks.register('xmldoc', Javadoc) { options.doclet = "com.manticore.tools.xmldoclet.XmlDoclet" title = "API $version" - options.addBooleanOption("rst", true) - if (Boolean.parseBoolean(System.getProperty("FLOATING_TOC", "true"))) { - options.addBooleanOption("withFloatingToc", true) - } options.addStringOption("basePackage", "net.sf.jsqlparser") options.addStringOption("filename", outFile.getName()) doLast { copy { - from rstFile + from outFile into layout.projectDirectory.dir("src/site/sphinx/").asFile } } @@ -229,7 +273,7 @@ test { maxHeapSize = "4G" // set JVM stack size - jvmArgs = ['-Xss2m', '--add-opens=java.base/java.lang=ALL-UNNAMED'] + jvmArgs = ['-Xss4m', '--add-opens=java.base/java.lang=ALL-UNNAMED'] jacoco { excludes = ['net/sf/jsqlparser/parser/CCJSqlParserTokenManager'] @@ -278,7 +322,7 @@ jacocoTestCoverageVerification { //@todo: temporarily increased to 7000, we need to bring that down to 5500 after accepting the Keywords PR maximum = 20000 - } + } excludes = [ 'net.sf.jsqlparser.util.validation.*', 'net.sf.jsqlparser.**.*Adapter', @@ -313,27 +357,31 @@ spotbugsMain { spotbugs { // fail only on P1 and without the net.sf.jsqlparser.parser.* excludeFilter = file("config/spotbugs/spotBugsExcludeFilter.xml") +} - // do not run over the test, although we should do that eventually - spotbugsTest.enabled = false +// do not run over the test, although we should do that eventually +tasks.named('spotbugsTest').configure { + enabled = false } pmd { + // later versions throw NPE + toolVersion = '7.17.0' + consoleOutput = true sourceSets = [sourceSets.main] // clear the ruleset in order to use configured rules only ruleSets = [] - - //rulesMinimumPriority = 1 + rulesMinimumPriority = 2 ruleSetFiles = files("config/pmd/ruleset.xml") +} - pmdMain { - excludes = [ - "build/generated/*" - , "**/net/sf/jsqlparser/parser/SimpleCharStream.java" - ] - } +tasks.named('pmdMain').configure { + excludes = [ + "build/generated/*" + , "**/net/sf/jsqlparser/parser/SimpleCharStream.java" + ] } checkstyle { @@ -396,28 +444,34 @@ tasks.register('renderRR') { } // Convert JJ file to EBNF - tasks.register("convertJJ", JavaExec) { - standardOutput = new FileOutputStream(new File(rrDir, "JSqlParserCC.ebnf")) - mainClass = "-jar" - args = [ - new File(rrDir, "convert.war").absolutePath, - layout.buildDirectory.dir("generated/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jj").get().asFile.absolutePath - ] - }.get().exec() + def ebnfFile = new File(rrDir, "JSqlParserCC.ebnf") + def jjFile = layout.buildDirectory.dir("generated/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jj").get().asFile.absolutePath + + def convertProc = new ProcessBuilder('java', '-jar', + new File(rrDir, "convert.war").absolutePath, + jjFile) + .redirectOutput(ebnfFile) + .redirectErrorStream(true) + .start() + if (convertProc.waitFor() != 0) { + throw new GradleException("Failed to convert JJ to EBNF") + } // Generate RR diagrams - tasks.register("generateRR", JavaExec) { - mainClass = "-jar" - args = [ - new File(rrDir, "rr.war").absolutePath, - "-noepsilon", - "-color:#4D88FF", - "-offset:0", - "-width:800", - "-out:${new File(rrDir, "JSqlParserCC.xhtml")}", - new File(rrDir, "JSqlParserCC.ebnf").absolutePath - ] - }.get().exec() + def rrProc = new ProcessBuilder('java', '-jar', + new File(rrDir, "rr.war").absolutePath, + "-noepsilon", + "-color:#4D88FF", + "-offset:0", + "-width:800", + "-out:${new File(rrDir, "JSqlParserCC.xhtml")}", + new File(rrDir, "JSqlParserCC.ebnf").absolutePath) + .redirectErrorStream(true) + .start() + rrProc.inputStream.eachLine { logger.info(it) } + if (rrProc.waitFor() != 0) { + throw new GradleException("Failed to generate RR diagrams") + } } } @@ -463,13 +517,13 @@ Version {{name}} tasks.register('updateKeywords', JavaExec) { group = "Execution" - description = "Run the main class with JavaExecTask" + description = "Generate the Reserved Keywords documentation" classpath = sourceSets.main.runtimeClasspath args = [ file('src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt').absolutePath , file('src/site/sphinx/keywords.rst').absolutePath ] - main("net.sf.jsqlparser.parser.ParserKeywordsUtils") + mainClass.set("net.sf.jsqlparser.parser.ParserKeywordsUtils") dependsOn(compileJava) } @@ -483,7 +537,7 @@ tasks.register('xslt', SaxonXsltTask) { stylesheet file('src/main/resources/rr/xhtml2rst.xsl') parameters( - "withFloatingToc": System.getProperty("FLOATING_TOC", "true"), + "withFloatingToc": System.getProperty("FLOATING_TOC", "false"), "isSnapshot": Boolean.toString(version.endsWith("-SNAPSHOT")) ) @@ -608,10 +662,6 @@ publishing { signing { - //def signingKey = findProperty("signingKey") - //def signingPassword = findProperty("signingPassword") - //useInMemoryPgpKeys(signingKey, signingPassword) - // don't sign SNAPSHOTS if (!version.endsWith('SNAPSHOT')) { sign publishing.publications.mavenJava @@ -662,9 +712,20 @@ 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 iterations = 5 timeOnIteration = '1s' -} +} \ No newline at end of file diff --git a/config/pmd/ruleset.xml b/config/pmd/ruleset.xml index acca2d6e9..45804d4c0 100644 --- a/config/pmd/ruleset.xml +++ b/config/pmd/ruleset.xml @@ -20,14 +20,15 @@ under the License. - The default ruleset used by the Maven PMD Plugin, when no other ruleset is specified. - It contains the rules of the old (pre PMD 6.0.0) rulesets java-basic, java-empty, java-imports, - java-unnecessary, java-unusedcode. + Custom PMD ruleset, compatible with PMD 7.x. - This ruleset might be used as a starting point for an own customized ruleset [0]. + Based on the old (pre PMD 6.0.0) rulesets java-basic, java-empty, java-imports, + java-unnecessary, java-unusedcode, migrated for PMD 7. - [0] https://pmd.github.io/latest/pmd_userdocs_making_rulesets.html - + This ruleset might be used as a starting point for an own customized ruleset [0]. + + [0] https://pmd.github.io/latest/pmd_userdocs_making_rulesets.html + @@ -46,6 +47,14 @@ under the License. + + + + + + @@ -63,14 +72,15 @@ under the License. - - + + + @@ -80,16 +90,10 @@ under the License. + + - - - - - - - - - + @@ -97,15 +101,14 @@ under the License. - - - + + - + - + \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index a4b76b953..b1b8ef56b 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index cea7a793a..a9db11550 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,9 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip networkTimeout=10000 +retries=0 +retryBackOffMs=500 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index f5feea6d6..379a6582e 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ ############################################################################## # -# Gradle start up script for POSIX generated by Gradle. +# gradlew start up script for POSIX generated by Gradle. # # Important for running: # @@ -29,7 +29,7 @@ # bash, then to run this script, type that shell name before the whole # command line, like: # -# ksh Gradle +# ksh gradlew # # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: @@ -57,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob//platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -86,8 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s -' "$PWD" ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -115,7 +114,6 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -173,7 +171,6 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) @@ -206,15 +203,14 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 9d21a2183..a51ec4f58 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -19,12 +19,12 @@ @if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem -@rem Gradle startup script for Windows +@rem gradlew startup script for Windows @rem @rem ########################################################################## -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions set DIRNAME=%~dp0 if "%DIRNAME%"=="" set DIRNAME=. @@ -51,7 +51,7 @@ echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :findJavaFromJavaHome set JAVA_HOME=%JAVA_HOME:"=% @@ -65,30 +65,18 @@ echo. 1>&2 echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo location of your Java installation. 1>&2 -goto fail +"%COMSPEC%" /c exit 1 :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +@rem Execute gradlew +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/pom.xml b/pom.xml index 5cdaea540..cd1e28b0b 100644 --- a/pom.xml +++ b/pom.xml @@ -1,15 +1,21 @@ + 4.0.0 + com.github.jsqlparser jsqlparser 5.4-SNAPSHOT - JSQLParser library + bundle + + JSQLParser library + JSqlParser parses an SQL statement and translate it into a hierarchy of Java classes. + The generated hierarchy can be navigated using the Visitor Pattern. + https://github.com/JSQLParser/JSqlParser 2004 + - JSQLParser + JSQLParser - bundle - https://github.com/JSQLParser/JSqlParser @@ -24,146 +30,281 @@ - - - javacc8-snapshots + + + Tobias Warneke + t.warneke@gmx.net + + + + + scm:git:https://github.com/JSQLParser/JSqlParser.git + scm:git:ssh://git@github.com:JSQLParser/JSqlParser.git + https://github.com/JSQLParser/JSqlParser.git + HEAD + + + + GitHub Issues + https://github.com/JSQLParser/JSqlParser/issues + + + + + + sonatype-nexus-snapshots + https://central.sonatype.com/repository/maven-snapshots/ + + false + true - false - https://central.sonatype.com/repository/maven-snapshots/ - - - ossrh-snapshots - https://central.sonatype.com/repository/maven-snapshots/ - true - false - - + + + + + + 11 + [17,24) + IfMatch + + UTF-8 + UTF-8 + + + 2026-01-01T00:00:00Z + + + + + 3.3.0 + 3.6.3 + 3.15.0 + 3.5.0 + 3.5.0 + 3.1.4 + 3.1.4 + 3.22.0 + 3.4.0 + 3.12.0 + 3.5.6 + 3.3.1 + 3.2.8 + 3.28.0 + 3.6.0 + 3.6.0 + 3.9.0 + 5.1.8 + 3.6.0 + 3.5.0 + 2.0.0 + 3.8.0 + 0.8.15 + 3.8.0 + 4.9.3.0 + 0.10.0 + + + 7.17.0 + 10.23.1 + + + + [8.1.1.1939,) + 5.14.4 + 5.23.0 + 3.27.7 + 3.0 + 2.18.0 + 3.18.0 + 2.3.232 + 1.37 + + + f22e0543 + + + + + + org.junit + junit-bom + ${junit.version} + pom + import + + + - - org.javacc - core - 8.1.0-SNAPSHOT - pom + com.manticore-projects.jsqlformatter + javacc-java + ${javacc.version} test - org.javacc.generator - java - 8.1.0-SNAPSHOT - pom + com.manticore-projects.jsqlformatter + javacc-core + ${javacc.version} test commons-io commons-io - 2.18.0 + ${commons-io.version} test - org.junit.jupiter - junit-jupiter - 5.11.4 - test + org.junit.jupiter + junit-jupiter + test org.mockito mockito-core - 5.15.2 + ${mockito.version} test org.mockito mockito-junit-jupiter - 5.15.2 + ${mockito.version} test org.assertj assertj-core - 3.27.3 + ${assertj.version} test org.apache.commons commons-lang3 - [3.18.0,) + ${commons-lang3.version} test com.h2database h2 - [2.3.232,) + ${h2.version} test - + org.hamcrest - hamcrest-all - 1.3 + hamcrest + ${hamcrest.version} test - + org.openjdk.jmh jmh-core - 1.37 - - - - - org.openjdk.jmh - jmh-generator-annprocess - 1.37 - provided + ${jmh.version} + test - - - - Tobias Warneke - t.warneke@gmx.net - - - - - - sonatype-nexus-staging - https://central.sonatype.com/repository/maven-releases - - - sonatype-nexus-snapshots - https://central.sonatype.com/repository/maven-snapshots/ - false - true - - + + + + + + org.apache.maven.plugins + maven-clean-plugin + ${maven-clean-plugin.version} + + + org.apache.maven.plugins + maven-resources-plugin + ${maven-resources-plugin.version} + + + org.apache.maven.plugins + maven-install-plugin + ${maven-install-plugin.version} + + + org.apache.maven.plugins + maven-deploy-plugin + ${maven-deploy-plugin.version} + + + org.apache.maven.plugins + maven-site-plugin + ${maven-site-plugin.version} + + + - - scm:git:https://github.com/JSQLParser/JSqlParser.git - scm:git:ssh://git@github.com:JSQLParser/JSqlParser.git - https://github.com/JSQLParser/JSqlParser.git - HEAD - + + + + org.apache.maven.plugins + maven-toolchains-plugin + ${maven-toolchains-plugin.version} + + + select-jdk-toolchain + + select-jdk-toolchain + + + + - - GitHub Issues - https://github.com/JSQLParser/JSqlParser/issues - + + org.apache.maven.plugins + maven-enforcer-plugin + ${maven-enforcer-plugin.version} + + + enforce + validate + + enforce + + + + + [3.9.0,) + + + + + + + - - - + org.codehaus.mojo exec-maven-plugin - 3.1.0 + ${exec-maven-plugin.version} net.sf.jsqlparser.parser.ParserKeywordsUtils @@ -172,11 +313,13 @@ + org.apache.maven.plugins maven-pmd-plugin - 3.21.2 + ${maven-pmd-plugin.version} + 2 ${project.basedir}/config/pmd/ruleset.xml @@ -187,37 +330,38 @@ **/net/sf/jsqlparser/parser/SimpleCharStream.java - target/generated-sources - target/generated-test-sources + ${project.build.directory}/generated-sources + ${project.build.directory}/generated-test-sources true pmd + process-sources check - process-sources net.sourceforge.pmd pmd-core - ${pmdVersion} + ${pmd.version} net.sourceforge.pmd pmd-java - ${pmdVersion} + ${pmd.version} + org.codehaus.mojo build-helper-maven-plugin - 3.6.0 + ${build-helper-maven-plugin.version} add-source @@ -234,33 +378,47 @@ + + org.apache.maven.plugins maven-compiler-plugin - 3.14.0 + ${maven-compiler-plugin.version} - 11 - 11 - true + ${maven.compiler.release} ${project.build.sourceEncoding} + true true 2000m + + true -J-Xss4M - true - - - org.openjdk.jmh - jmh-generator-annprocess - 1.37 - - + + + + default-testCompile + + testCompile + + + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh.version} + + + + + + org.javacc.plugin javacc-maven-plugin - 3.0.3 + ${javacc-maven-plugin.version} javacc @@ -269,45 +427,35 @@ jjtree-javacc - java + + -CODE_GENERATOR:"Java" + -GRAMMAR_ENCODING:"UTF-8" + + + -GRAMMAR_ENCODING:"UTF-8" + -CODE_GENERATOR:"Java" + - org.javacc.generator - java - 8.1.0-SNAPSHOT + com.manticore-projects.jsqlformatter + javacc-java + ${javacc.version} - org.javacc - core - 8.1.0-SNAPSHOT + com.manticore-projects.jsqlformatter + javacc-core + ${javacc.version} - - org.apache.maven.plugins - maven-eclipse-plugin - 2.9 - - - /target/generated-sources/javacc - - - - - org.apache.maven.plugins - maven-resources-plugin - 3.3.0 - - ${project.build.sourceEncoding} - - + org.codehaus.mojo license-maven-plugin - 2.0.0 + ${license-maven-plugin.version} false false @@ -320,23 +468,19 @@ - first + update-file-header + process-sources update-file-header - process-sources + org.apache.maven.plugins maven-release-plugin - - 3.0.0-M7 + ${maven-release-plugin.version} true false @@ -344,88 +488,105 @@ sign-release-artifacts + org.apache.maven.plugins maven-source-plugin - 3.2.1 + ${maven-source-plugin.version} attach-sources - jar + jar-no-fork + org.apache.maven.plugins maven-javadoc-plugin - 3.11.2 + ${maven-javadoc-plugin.version} attach-javadocs + + jar + - ${javadoc.opts} net.sf.jsqlparser.parser none + + true true 2g 800m - -J-Xss4m + + -J-Xss4m + - - jar - - - org.apache.maven.plugins - maven-jar-plugin - 3.4.2 - - - - net.sf.jsqlparser - - - - - - org.eluder.coveralls - coveralls-maven-plugin - 4.3.0 - + + org.apache.felix maven-bundle-plugin - 5.1.8 + ${maven-bundle-plugin.version} true + + + net.sf.jsqlparser + + + org.apache.maven.plugins maven-surefire-plugin - 3.5.2 + ${maven-surefire-plugin.version} false false + + @{jacocoArgLine} --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED -Xmx2G -Xms800m -Xss4m + org.jacoco jacoco-maven-plugin - 0.8.11 + ${jacoco-maven-plugin.version} + prepare-agent prepare-agent + + + jacocoArgLine + report @@ -436,108 +597,97 @@ + + com.diffplug.spotless spotless-maven-plugin - 2.43.0 + ${spotless-maven-plugin.version} - + origin/master - - *.md .gitignore - - - + + true 4 - - src/main/java/**/*.java src/test/java/**/*.java - - - - - + + + config/formatter/eclipse-java-google-style.xml - - + + org.sonatype.central central-publishing-maven-plugin - 0.8.0 + ${central-publishing-maven-plugin.version} true - sonatype-nexus + sonatype-nexus - + - + org.apache.maven.plugins maven-surefire-report-plugin - 3.0.0-M7 + ${maven-surefire-plugin.version} ${project.reporting.outputDirectory}/testresults - -Xmx2G -Xms800m -Xss4m org.apache.maven.plugins maven-javadoc-plugin - 3.4.1 + ${maven-javadoc-plugin.version} true none + + true 2g 800m - -J-Xss2m - - + + -J-Xss2m + org.apache.maven.plugins maven-project-info-reports-plugin - 3.4.1 + ${maven-project-info-reports-plugin.version} org.apache.maven.plugins maven-jxr-plugin - 3.3.0 + ${maven-jxr-plugin.version} - - org.codehaus.mojo - findbugs-maven-plugin - 3.0.5 + com.github.spotbugs + spotbugs-maven-plugin + ${spotbugs-maven-plugin.version} @@ -556,7 +706,7 @@ org.apache.maven.plugins maven-gpg-plugin - 1.6 + ${maven-gpg-plugin.version} sign-artifacts @@ -565,7 +715,12 @@ sign - f22e0543 + ${gpg.keyname} + + + --pinentry-mode + loopback + @@ -573,21 +728,20 @@ - + check.sources !skipCheckSources - [11,) org.apache.maven.plugins maven-checkstyle-plugin - 3.3.1 + ${maven-checkstyle-plugin.version} verify-style @@ -600,38 +754,33 @@ true true + ${project.build.sourceDirectory} **/module-info.java,**/net/sf/jsqlparser/parser/SimpleCharStream.java - - - + + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + @@ -640,34 +789,24 @@ com.puppycrawl.tools checkstyle - ${checkStyleVersion} + ${checkstyle.version} + - + skip.all - - false - true true + true true true - - - UTF-8 - 6.55.0 - 10.14.0 - - - JSqlParser parses an SQL statement and translate it into a hierarchy of Java classes. - The generated hierarchy can be navigated using the Visitor Pattern. - + \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 000000000..d0322b0de --- /dev/null +++ b/settings.gradle @@ -0,0 +1,5 @@ +/* + * This file was generated by the Gradle 'init' task. + */ + +rootProject.name = 'JSQLParser' diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 6765fe187..ada4bfdf4 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -27,6 +27,7 @@ exports net.sf.jsqlparser.statement.comment; exports net.sf.jsqlparser.statement.create.function; exports net.sf.jsqlparser.statement.create.index; + exports net.sf.jsqlparser.statement.create.policy; exports net.sf.jsqlparser.statement.create.procedure; exports net.sf.jsqlparser.statement.create.schema; exports net.sf.jsqlparser.statement.create.sequence; @@ -40,6 +41,7 @@ exports net.sf.jsqlparser.statement.grant; exports net.sf.jsqlparser.statement.imprt; exports net.sf.jsqlparser.statement.insert; + exports net.sf.jsqlparser.statement.lock; exports net.sf.jsqlparser.statement.merge; exports net.sf.jsqlparser.statement.piped; exports net.sf.jsqlparser.statement.refresh; diff --git a/src/main/java/net/sf/jsqlparser/expression/AnalyticExpression.java b/src/main/java/net/sf/jsqlparser/expression/AnalyticExpression.java index 0c0d11146..d94d4ef2e 100644 --- a/src/main/java/net/sf/jsqlparser/expression/AnalyticExpression.java +++ b/src/main/java/net/sf/jsqlparser/expression/AnalyticExpression.java @@ -9,6 +9,7 @@ */ package net.sf.jsqlparser.expression; +import java.util.Locale; import net.sf.jsqlparser.expression.operators.relational.ExpressionList; import net.sf.jsqlparser.parser.ASTNodeAccessImpl; import net.sf.jsqlparser.statement.select.Limit; @@ -52,6 +53,8 @@ public class AnalyticExpression extends ASTNodeAccessImpl implements Expression private Limit limit = null; + private List keywordArguments = null; + public AnalyticExpression() {} public AnalyticExpression(Function function) { @@ -82,6 +85,7 @@ public AnalyticExpression(Function function) { this.onOverflowTruncate = function.getOnOverflowTruncate(); this.limit = function.getLimit(); this.keep = function.getKeep(); + this.keywordArguments = function.getKeywordArguments(); } @@ -240,7 +244,8 @@ public AnalyticExpression setHavingClause(Function.HavingClause havingClause) { public AnalyticExpression setHavingClause(String havingType, Expression expression) { this.havingClause = new Function.HavingClause( - Function.HavingClause.HavingType.valueOf(havingType.trim().toUpperCase()), + Function.HavingClause.HavingType + .valueOf(havingType.trim().toUpperCase(Locale.ROOT)), expression); return this; } @@ -263,6 +268,14 @@ public AnalyticExpression setLimit(Limit limit) { return this; } + public List getKeywordArguments() { + return keywordArguments; + } + + public void setKeywordArguments(List keywordArguments) { + this.keywordArguments = keywordArguments; + } + @Override @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity", "PMD.MissingBreakInSwitch"}) @@ -313,6 +326,13 @@ public String toString() { b.append(limit); } + // Generic keyword arguments (e.g. SEPARATOR ',') + if (keywordArguments != null) { + for (Function.KeywordArgument ka : keywordArguments) { + ka.appendTo(b); + } + } + b.append(") "); if (keep != null) { b.append(keep).append(" "); diff --git a/src/main/java/net/sf/jsqlparser/expression/AnalyticType.java b/src/main/java/net/sf/jsqlparser/expression/AnalyticType.java index 2738849c6..ea083c902 100644 --- a/src/main/java/net/sf/jsqlparser/expression/AnalyticType.java +++ b/src/main/java/net/sf/jsqlparser/expression/AnalyticType.java @@ -9,10 +9,12 @@ */ package net.sf.jsqlparser.expression; +import java.util.Locale; + public enum AnalyticType { OVER, WITHIN_GROUP, WITHIN_GROUP_OVER, FILTER_ONLY; public static AnalyticType from(String type) { - return Enum.valueOf(AnalyticType.class, type.toUpperCase()); + return Enum.valueOf(AnalyticType.class, type.toUpperCase(Locale.ROOT)); } } diff --git a/src/main/java/net/sf/jsqlparser/expression/AnyType.java b/src/main/java/net/sf/jsqlparser/expression/AnyType.java index fec6fb09d..da5495d07 100644 --- a/src/main/java/net/sf/jsqlparser/expression/AnyType.java +++ b/src/main/java/net/sf/jsqlparser/expression/AnyType.java @@ -9,10 +9,12 @@ */ package net.sf.jsqlparser.expression; +import java.util.Locale; + public enum AnyType { ANY, SOME, ALL; public static AnyType from(String type) { - return Enum.valueOf(AnyType.class, type.toUpperCase()); + return Enum.valueOf(AnyType.class, type.toUpperCase(Locale.ROOT)); } } diff --git a/src/main/java/net/sf/jsqlparser/expression/CastExpression.java b/src/main/java/net/sf/jsqlparser/expression/CastExpression.java index f06682068..66af39ed5 100644 --- a/src/main/java/net/sf/jsqlparser/expression/CastExpression.java +++ b/src/main/java/net/sf/jsqlparser/expression/CastExpression.java @@ -9,6 +9,7 @@ */ package net.sf.jsqlparser.expression; +import java.util.Locale; import net.sf.jsqlparser.parser.ASTNodeAccessImpl; import net.sf.jsqlparser.statement.create.table.ColDataType; import net.sf.jsqlparser.statement.create.table.ColumnDefinition; @@ -276,7 +277,8 @@ public enum DataType { ARRAY, BIT, BITSTRING, BLOB, BYTEA, BINARY, VARBINARY, BYTES, BOOLEAN, BOOL, ENUM, INTERVAL, LIST, MAP, STRUCT, TINYINT, INT1, SMALLINT, INT2, SHORT, INTEGER, INT4, INT, SIGNED, BIGINT, INT8, LONG, HUGEINT, UTINYINT, USMALLINT, UINTEGER, UBIGINT, UHUGEINT, DECIMAL, NUMBER, NUMERIC, REAL, FLOAT4, FLOAT, DOUBLE, DOUBLE_PRECISION, FLOAT8, FLOAT64, UUID, VARCHAR, NVARCHAR, CHAR, NCHAR, BPCHAR, STRING, TEXT, CLOB, DATE, TIME, TIME_WITHOUT_TIME_ZONE, TIMETZ, TIME_WITH_TIME_ZONE, TIMESTAMP_NS, TIMESTAMP, TIMESTAMP_WITHOUT_TIME_ZONE, DATETIME, TIMESTAMP_MS, TIMESTAMP_S, TIMESTAMPTZ, TIMESTAMP_WITH_TIME_ZONE, UNKNOWN, VARBYTE, JSON; public static DataType from(String typeStr) { - Matcher matcher = PATTERN.matcher(typeStr.trim().replaceAll("\\s+", "_").toUpperCase()); + Matcher matcher = PATTERN.matcher( + typeStr.trim().replaceAll("\\s+", "_").toUpperCase(Locale.ROOT)); if (matcher.find()) { try { return Enum.valueOf(DataType.class, matcher.group(0)); diff --git a/src/main/java/net/sf/jsqlparser/expression/ConnectByPriorOperator.java b/src/main/java/net/sf/jsqlparser/expression/ConnectByPriorOperator.java index 98421e2bb..45c2fde6a 100644 --- a/src/main/java/net/sf/jsqlparser/expression/ConnectByPriorOperator.java +++ b/src/main/java/net/sf/jsqlparser/expression/ConnectByPriorOperator.java @@ -35,15 +35,26 @@ * @author are */ public class ConnectByPriorOperator extends ASTNodeAccessImpl implements Expression { - private final Column column; + private final Expression expression; + @Deprecated public ConnectByPriorOperator(Column column) { - this.column = Objects.requireNonNull(column, + this.expression = Objects.requireNonNull(column, "The COLUMN of the ConnectByPrior Operator must not be null"); } - public Column getColumn() { - return column; + public ConnectByPriorOperator(Expression column) { + this.expression = Objects.requireNonNull(column, + "The COLUMN of the ConnectByPrior Operator must not be null"); + } + + @Deprecated + public Expression getColumn() { + return getExpression(); + } + + public Expression getExpression() { + return expression; } @Override @@ -52,7 +63,7 @@ public T accept(ExpressionVisitor expressionVisitor, S context) { } public StringBuilder appendTo(StringBuilder builder) { - builder.append("PRIOR ").append(column); + builder.append("PRIOR ").append(expression); return builder; } @@ -60,4 +71,4 @@ public StringBuilder appendTo(StringBuilder builder) { public String toString() { return appendTo(new StringBuilder()).toString(); } -} \ No newline at end of file +} diff --git a/src/main/java/net/sf/jsqlparser/expression/ConnectByRootOperator.java b/src/main/java/net/sf/jsqlparser/expression/ConnectByRootOperator.java index 6942f0787..776dc031e 100644 --- a/src/main/java/net/sf/jsqlparser/expression/ConnectByRootOperator.java +++ b/src/main/java/net/sf/jsqlparser/expression/ConnectByRootOperator.java @@ -34,15 +34,26 @@ * @author are */ public class ConnectByRootOperator extends ASTNodeAccessImpl implements Expression { - private final Column column; + private final Expression expression; + @Deprecated public ConnectByRootOperator(Column column) { - this.column = Objects.requireNonNull(column, + this.expression = Objects.requireNonNull(column, "The COLUMN of the ConnectByRoot Operator must not be null"); } - public Column getColumn() { - return column; + public ConnectByRootOperator(Expression column) { + this.expression = Objects.requireNonNull(column, + "The EXPRESSION of the ConnectByRoot Operator must not be null"); + } + + @Deprecated + public Expression getColumn() { + return expression; + } + + public Expression getExpression() { + return expression; } @Override @@ -51,7 +62,7 @@ public T accept(ExpressionVisitor expressionVisitor, S context) { } public StringBuilder appendTo(StringBuilder builder) { - builder.append("CONNECT_BY_ROOT ").append(column); + builder.append("CONNECT_BY_ROOT ").append(expression); return builder; } diff --git a/src/main/java/net/sf/jsqlparser/expression/DateTimeLiteralExpression.java b/src/main/java/net/sf/jsqlparser/expression/DateTimeLiteralExpression.java index ccb15db4b..c2873bfb0 100644 --- a/src/main/java/net/sf/jsqlparser/expression/DateTimeLiteralExpression.java +++ b/src/main/java/net/sf/jsqlparser/expression/DateTimeLiteralExpression.java @@ -9,6 +9,7 @@ */ package net.sf.jsqlparser.expression; +import java.util.Locale; import net.sf.jsqlparser.parser.ASTNodeAccessImpl; public class DateTimeLiteralExpression extends ASTNodeAccessImpl implements Expression { @@ -56,7 +57,7 @@ public enum DateTime { DATE, DATETIME, TIME, TIMESTAMP, TIMESTAMPTZ; public static DateTime from(String dateTimeStr) { - return Enum.valueOf(DateTime.class, dateTimeStr.toUpperCase()); + return Enum.valueOf(DateTime.class, dateTimeStr.toUpperCase(Locale.ROOT)); } } } diff --git a/src/main/java/net/sf/jsqlparser/expression/DateUnitExpression.java b/src/main/java/net/sf/jsqlparser/expression/DateUnitExpression.java new file mode 100644 index 000000000..922c0c419 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/expression/DateUnitExpression.java @@ -0,0 +1,51 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2019 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.expression; + +import java.util.Locale; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; + +import java.util.Objects; + +public class DateUnitExpression extends ASTNodeAccessImpl implements Expression { + + private final DateUnit type; + + public DateUnitExpression(DateUnit type) { + this.type = Objects.requireNonNull(type); + } + + public DateUnitExpression(String DateUnitStr) { + this.type = Objects.requireNonNull(DateUnit.from(DateUnitStr)); + } + + public DateUnit getType() { + return type; + } + + + @Override + public T accept(ExpressionVisitor expressionVisitor, S context) { + return expressionVisitor.visit(this, context); + } + + @Override + public String toString() { + return type.toString(); + } + + public enum DateUnit { + CENTURY, DECADE, YEAR, QUARTER, MONTH, WEEK, DAY, HOUR, MINUTE, SECOND, MILLISECOND, MICROSECOND, NANOSECOND; + + public static DateUnit from(String UnitStr) { + return Enum.valueOf(DateUnit.class, UnitStr.toUpperCase(Locale.ROOT)); + } + } +} diff --git a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java index 8b5ade13d..2eb909a98 100644 --- a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java +++ b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitor.java @@ -9,6 +9,7 @@ */ package net.sf.jsqlparser.expression; +import java.util.List; import net.sf.jsqlparser.expression.operators.arithmetic.Addition; import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseAnd; import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseLeftShift; @@ -68,8 +69,6 @@ import net.sf.jsqlparser.statement.select.Select; import net.sf.jsqlparser.statement.update.UpdateSet; -import java.util.List; - public interface ExpressionVisitor { default T visitExpressions(ExpressionList expressions, S context) { @@ -622,6 +621,14 @@ default void visit(ArrayConstructor arrayConstructor) { this.visit(arrayConstructor, null); } + default T visit(MapExpression mapExpression, S context) { + return null; + } + + default void visit(MapExpression mapExpression) { + this.visit(mapExpression, null); + } + T visit(VariableAssignment variableAssignment, S context); default void visit(VariableAssignment variableAssignment) { @@ -652,6 +659,22 @@ default void visit(JsonFunction jsonFunction) { this.visit(jsonFunction, null); } + default T visit(JsonTableFunction jsonTableFunction, S context) { + return visit((Function) jsonTableFunction, context); + } + + default void visit(JsonTableFunction jsonTableFunction) { + this.visit(jsonTableFunction, null); + } + + default T visit(XmlTableFunction xmlTableFunction, S context) { + return visit((Function) xmlTableFunction, context); + } + + default void visit(XmlTableFunction xmlTableFunction) { + this.visit(xmlTableFunction, null); + } + T visit(ConnectByRootOperator connectByRootOperator, S context); default void visit(ConnectByRootOperator connectByRootOperator) { @@ -722,6 +745,12 @@ default void visit(RangeExpression rangeExpression) { this.visit(rangeExpression, null); } + T visit(TernaryExpression ternaryExpression, S context); + + default void visit(TernaryExpression ternaryExpression) { + this.visit(ternaryExpression, null); + } + T visit(TSQLLeftJoin tsqlLeftJoin, S context); default void visit(TSQLLeftJoin tsqlLeftJoin) { @@ -779,4 +808,18 @@ default void visit(Inverse inverse) { T visit(CosineSimilarity cosineSimilarity, S context); T visit(FromQuery fromQuery, S context); + + T visit(DateUnitExpression dateUnitExpression, S context); + + T visit(KeyExpression keyExpression, S context); + + default void visit(KeyExpression keyExpression) { + this.visit(keyExpression, null); + } + + T visit(PostgresNamedFunctionParameter postgresNamedFunctionParameter, S context); + + default void visit(PostgresNamedFunctionParameter postgresNamedFunctionParameter) { + this.visit(postgresNamedFunctionParameter, null); + } } diff --git a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java index 96d80d514..274d4e00a 100644 --- a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java @@ -9,6 +9,11 @@ */ package net.sf.jsqlparser.expression; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; +import java.util.Optional; import net.sf.jsqlparser.expression.operators.arithmetic.Addition; import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseAnd; import net.sf.jsqlparser.expression.operators.arithmetic.BitwiseLeftShift; @@ -73,11 +78,6 @@ import net.sf.jsqlparser.statement.select.UnPivot; import net.sf.jsqlparser.statement.select.WithItem; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Optional; - @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.UncommentedEmptyMethodBody"}) public class ExpressionVisitorAdapter implements ExpressionVisitor, PivotVisitor, SelectItemVisitor { @@ -112,6 +112,9 @@ public T visit(Function function, S context) { if (function.getParameters() != null) { subExpressions.addAll(function.getParameters()); } + if (function.getChainedParameters() != null) { + subExpressions.addAll(function.getChainedParameters()); + } if (function.getKeep() != null) { subExpressions.add(function.getKeep()); } @@ -691,6 +694,16 @@ public T visit(ArrayConstructor arrayConstructor, S context) { return visitExpressions(arrayConstructor, context, arrayConstructor.getExpressions()); } + @Override + public T visit(MapExpression mapExpression, S context) { + ArrayList subExpressions = new ArrayList<>(); + for (Map.Entry entry : mapExpression.getEntries()) { + subExpressions.add(entry.getKey()); + subExpressions.add(entry.getValue()); + } + return visitExpressions(mapExpression, context, subExpressions); + } + @Override public T visit(VariableAssignment variableAssignment, S context) { return visitExpressions(variableAssignment, context, variableAssignment.getVariable(), @@ -722,12 +735,45 @@ public T visit(JsonAggregateFunction jsonAggregateFunction, S context) { @Override public T visit(JsonFunction jsonFunction, S context) { ArrayList subExpressions = new ArrayList<>(); + for (JsonKeyValuePair keyValuePair : jsonFunction.getKeyValuePairs()) { + if (keyValuePair.getKey() instanceof Expression) { + subExpressions.add((Expression) keyValuePair.getKey()); + } + if (keyValuePair.getValue() instanceof Expression) { + subExpressions.add((Expression) keyValuePair.getValue()); + } + } for (JsonFunctionExpression expr : jsonFunction.getExpressions()) { subExpressions.add(expr.getExpression()); } + if (jsonFunction.getInputExpression() != null) { + subExpressions.add(jsonFunction.getInputExpression().getExpression()); + } + if (jsonFunction.getJsonPathExpression() != null) { + subExpressions.add(jsonFunction.getJsonPathExpression()); + } + subExpressions.addAll(jsonFunction.getPassingExpressions()); + if (jsonFunction.getOnEmptyBehavior() != null + && jsonFunction.getOnEmptyBehavior().getExpression() != null) { + subExpressions.add(jsonFunction.getOnEmptyBehavior().getExpression()); + } + if (jsonFunction.getOnErrorBehavior() != null + && jsonFunction.getOnErrorBehavior().getExpression() != null) { + subExpressions.add(jsonFunction.getOnErrorBehavior().getExpression()); + } return visitExpressions(jsonFunction, context, subExpressions); } + @Override + public T visit(JsonTableFunction jsonTableFunction, S context) { + return visitExpressions(jsonTableFunction, context, jsonTableFunction.getAllExpressions()); + } + + @Override + public T visit(XmlTableFunction xmlTableFunction, S context) { + return visitExpressions(xmlTableFunction, context, xmlTableFunction.getAllExpressions()); + } + @Override public T visit(ConnectByRootOperator connectByRootOperator, S context) { return connectByRootOperator.getColumn().accept(this, context); @@ -738,11 +784,21 @@ public T visit(ConnectByPriorOperator connectByPriorOperator, S context) { return connectByPriorOperator.getColumn().accept(this, context); } + @Override + public T visit(KeyExpression keyExpression, S context) { + return keyExpression.getExpression().accept(this, context); + } + @Override public T visit(OracleNamedFunctionParameter oracleNamedFunctionParameter, S context) { return oracleNamedFunctionParameter.getExpression().accept(this, context); } + @Override + public T visit(PostgresNamedFunctionParameter postgresNamedFunctionParameter, S context) { + return postgresNamedFunctionParameter.getExpression().accept(this, context); + } + @Override public T visit(GeometryDistance geometryDistance, S context) { return visitBinaryExpression(geometryDistance, context); @@ -777,6 +833,12 @@ public T visit(RangeExpression rangeExpression, S context) { rangeExpression.getEndExpression()); } + @Override + public T visit(TernaryExpression ternaryExpression, S context) { + return visitExpressions(ternaryExpression, context, ternaryExpression.getCondition(), + ternaryExpression.getThenExpression(), ternaryExpression.getElseExpression()); + } + @Override public T visit(TSQLLeftJoin tsqlLeftJoin, S context) { return visitBinaryExpression(tsqlLeftJoin, context); @@ -840,4 +902,9 @@ public T visit(FromQuery fromQuery, S context) { return null; } + @Override + public T visit(DateUnitExpression dateUnitExpression, S context) { + return null; + } + } diff --git a/src/main/java/net/sf/jsqlparser/expression/Function.java b/src/main/java/net/sf/jsqlparser/expression/Function.java index d8ef6cb2e..581b13cf5 100644 --- a/src/main/java/net/sf/jsqlparser/expression/Function.java +++ b/src/main/java/net/sf/jsqlparser/expression/Function.java @@ -9,6 +9,7 @@ */ package net.sf.jsqlparser.expression; +import java.util.Locale; import net.sf.jsqlparser.expression.operators.relational.ExpressionList; import net.sf.jsqlparser.expression.operators.relational.NamedExpressionList; import net.sf.jsqlparser.parser.ASTNodeAccessImpl; @@ -16,9 +17,12 @@ import net.sf.jsqlparser.statement.select.Limit; import net.sf.jsqlparser.statement.select.OrderByElement; +import java.io.Serializable; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Objects; /** * A function as MAX,COUNT... @@ -26,6 +30,7 @@ public class Function extends ASTNodeAccessImpl implements Expression { private List nameparts; private ExpressionList parameters; + private ExpressionList chainedParameters; private NamedExpressionList namedParameters; private boolean allColumns = false; private boolean distinct = false; @@ -42,6 +47,14 @@ public class Function extends ASTNodeAccessImpl implements Expression { private String onOverflowTruncate = null; private String extraKeyword = null; + /** + * Generic keyword arguments captured inside function parentheses, e.g. + * {@code GROUP_CONCAT(col ORDER BY col SEPARATOR ',')} where {@code SEPARATOR ','} is a keyword + * argument. This acts as a catch-all for dialect-specific {@code KEYWORD expr} pairs that don't + * have dedicated grammar branches. + */ + private List keywordArguments = null; + public Function() {} public Function(String name, Expression... parameters) { @@ -140,7 +153,8 @@ public Function setHavingClause(HavingClause havingClause) { public Function setHavingClause(String havingType, Expression expression) { this.havingClause = new HavingClause( - HavingClause.HavingType.valueOf(havingType.trim().toUpperCase()), expression); + HavingClause.HavingType.valueOf(havingType.trim().toUpperCase(Locale.ROOT)), + expression); return this; } @@ -192,6 +206,20 @@ public void setParameters(ExpressionList list) { parameters = list; } + /** + * Additional function-call parameters for dialects that support chained function calls, e.g. + * quantile(0.95)(cost) in ClickHouse. + * + * @return the chained parameters of the function (if any, else null) + */ + public ExpressionList getChainedParameters() { + return chainedParameters; + } + + public void setChainedParameters(ExpressionList chainedParameters) { + this.chainedParameters = chainedParameters; + } + /** * the parameters might be named parameters, e.g. substring('foobar' from 2 for 3) * @@ -266,6 +294,53 @@ public Function setExtraKeyword(String extraKeyword) { return this; } + // ── Generic keyword argument support ─────────────────────────────── + + /** + * Returns the list of generic keyword arguments, e.g. {@code SEPARATOR ','}. + * + * @return keyword arguments or {@code null} + */ + public List getKeywordArguments() { + return keywordArguments; + } + + public void setKeywordArguments(List keywordArguments) { + this.keywordArguments = keywordArguments; + } + + /** + * Adds a single keyword argument (appends to the list, creating it if needed). + */ + public Function addKeywordArgument(String keyword, Expression expression) { + if (this.keywordArguments == null) { + this.keywordArguments = new ArrayList<>(); + } + this.keywordArguments.add(new KeywordArgument(keyword, expression)); + return this; + } + + public Function withKeywordArguments(List keywordArguments) { + this.keywordArguments = keywordArguments; + return this; + } + + /** + * Convenience lookup: returns the expression for the first keyword argument matching the given + * keyword (case-insensitive), or {@code null}. + */ + public Expression getKeywordArgumentValue(String keyword) { + if (keywordArguments == null) { + return null; + } + for (KeywordArgument ka : keywordArguments) { + if (ka.getKeyword().equalsIgnoreCase(keyword)) { + return ka.getExpression(); + } + } + return null; + } + @Override @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"}) public String toString() { @@ -324,6 +399,13 @@ public String toString() { b.append(" ON OVERFLOW ").append(onOverflowTruncate); } + // Generic keyword arguments (e.g. SEPARATOR ',') + if (keywordArguments != null) { + for (KeywordArgument ka : keywordArguments) { + ka.appendTo(b); + } + } + b.append(")"); params = b.toString(); } else { @@ -335,6 +417,10 @@ public String toString() { String ans = getName() + params; + if (chainedParameters != null) { + ans += "(" + chainedParameters + ")"; + } + if (nullHandling != null && isIgnoreNullsOutside()) { switch (nullHandling) { case IGNORE_NULLS: @@ -393,6 +479,11 @@ public Function withParameters(Expression... parameters) { return withParameters(new ExpressionList<>(parameters)); } + public Function withChainedParameters(ExpressionList chainedParameters) { + this.setChainedParameters(chainedParameters); + return this; + } + public Function withNamedParameters(NamedExpressionList namedParameters) { this.setNamedParameters(namedParameters); return this; @@ -438,6 +529,74 @@ public enum NullHandling { IGNORE_NULLS, RESPECT_NULLS; } + // ── KeywordArgument inner class ──────────────────────────────────── + + /** + * Represents a generic {@code KEYWORD expression} pair inside a function call. + *

+ * Examples: + *

    + *
  • {@code GROUP_CONCAT(col SEPARATOR ',')} → keyword="SEPARATOR", expression=','
  • + *
+ */ + public static class KeywordArgument implements Serializable { + private String keyword; + private Expression expression; + + public KeywordArgument() {} + + public KeywordArgument(String keyword, Expression expression) { + this.keyword = keyword; + this.expression = expression; + } + + public String getKeyword() { + return keyword; + } + + public KeywordArgument setKeyword(String keyword) { + this.keyword = keyword; + return this; + } + + public Expression getExpression() { + return expression; + } + + public KeywordArgument setExpression(Expression expression) { + this.expression = expression; + return this; + } + + public StringBuilder appendTo(StringBuilder builder) { + builder.append(" ").append(keyword).append(" ").append(expression); + return builder; + } + + @Override + public String toString() { + return keyword + " " + expression; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof KeywordArgument)) { + return false; + } + KeywordArgument that = (KeywordArgument) o; + return Objects.equals(keyword, that.keyword) + && Objects.equals(expression, that.expression); + } + + @Override + public int hashCode() { + return Objects.hash(keyword, expression); + } + } + public static class HavingClause extends ASTNodeAccessImpl implements Expression { HavingType havingType; Expression expression; diff --git a/src/main/java/net/sf/jsqlparser/expression/HexValue.java b/src/main/java/net/sf/jsqlparser/expression/HexValue.java index cba9a3ac1..55ee5d0e9 100644 --- a/src/main/java/net/sf/jsqlparser/expression/HexValue.java +++ b/src/main/java/net/sf/jsqlparser/expression/HexValue.java @@ -9,6 +9,7 @@ */ package net.sf.jsqlparser.expression; +import java.util.Locale; import net.sf.jsqlparser.parser.ASTNodeAccessImpl; import java.nio.charset.StandardCharsets; @@ -60,7 +61,7 @@ public String toString() { } public String getDigits() { - return value.toUpperCase().startsWith("0X") + return value.toUpperCase(Locale.ROOT).startsWith("0X") ? value.substring(2) : value.substring(2, value.length() - 1); } diff --git a/src/main/java/net/sf/jsqlparser/expression/IntervalExpression.java b/src/main/java/net/sf/jsqlparser/expression/IntervalExpression.java index 9c028c769..7e496b4ce 100644 --- a/src/main/java/net/sf/jsqlparser/expression/IntervalExpression.java +++ b/src/main/java/net/sf/jsqlparser/expression/IntervalExpression.java @@ -18,6 +18,7 @@ public class IntervalExpression extends ASTNodeAccessImpl implements Expression private final boolean intervalKeyword; private String parameter = null; private String intervalType = null; + private IntervalQualifier intervalQualifier = null; private Expression expression = null; public IntervalExpression() { @@ -47,12 +48,42 @@ public void setParameter(String parameter) { this.parameter = parameter; } + /** + * Returns the non-standard single-identifier interval type (e.g. MySQL {@code INTERVAL 1 foo}), + * or {@code null} when a structured {@link IntervalQualifier} is present. + * + * @deprecated the structured {@link #getIntervalQualifier()} is the canonical representation. + * This legacy field is kept only for the non-standard single-identifier form. + * @return the legacy interval type, or {@code null} if a qualifier is set + */ + @Deprecated public String getIntervalType() { return intervalType; } + /** + * Sets the non-standard single-identifier interval type (e.g. MySQL {@code INTERVAL 1 foo}) and + * clears any structured qualifier. + * + * @deprecated use {@link #setIntervalQualifier(IntervalQualifier)} for the standard form. + * @param intervalType the legacy interval type + */ + @Deprecated public void setIntervalType(String intervalType) { this.intervalType = intervalType; + this.intervalQualifier = null; + } + + public IntervalQualifier getIntervalQualifier() { + return intervalQualifier; + } + + /** + * Sets the structured interval qualifier and clears the legacy interval type. + */ + public void setIntervalQualifier(IntervalQualifier intervalQualifier) { + this.intervalQualifier = intervalQualifier; + this.intervalType = null; } public Expression getExpression() { @@ -67,7 +98,9 @@ public void setExpression(Expression expression) { public String toString() { return (intervalKeyword ? "INTERVAL " : "") + Objects.toString(expression, parameter) - + (intervalType != null ? " " + intervalType : ""); + + (intervalQualifier != null + ? " " + intervalQualifier.toString() + : (intervalType != null ? " " + intervalType : "")); } @Override @@ -80,11 +113,22 @@ public IntervalExpression withParameter(String parameter) { return this; } + /** + * @deprecated use {@link #withIntervalQualifier(IntervalQualifier)} for the standard form. + * @param intervalType the legacy interval type + * @return this instance + */ + @Deprecated public IntervalExpression withIntervalType(String intervalType) { this.setIntervalType(intervalType); return this; } + public IntervalExpression withIntervalQualifier(IntervalQualifier intervalQualifier) { + this.setIntervalQualifier(intervalQualifier); + return this; + } + public IntervalExpression withExpression(Expression expression) { this.setExpression(expression); return this; diff --git a/src/main/java/net/sf/jsqlparser/expression/IntervalQualifier.java b/src/main/java/net/sf/jsqlparser/expression/IntervalQualifier.java new file mode 100644 index 000000000..a9676c69c --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/expression/IntervalQualifier.java @@ -0,0 +1,123 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2019 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.expression; + +import java.io.Serializable; +import java.util.Objects; + +/** + * Models an SQL interval qualifier as defined by the SQL standard (see + * PostgreSQL: Interval + * Input), e.g. {@code YEAR}, {@code DAY TO SECOND}, {@code DAY(9) TO SECOND} or + * {@code SECOND(2, 4)}. + *

+ * It is shared by {@link IntervalExpression} (the {@code INTERVAL ...} expression literal, used in + * DML) and by {@code net.sf.jsqlparser.statement.create.table.ColDataType} (the {@code INTERVAL} + * column / cast target type, used in DDL), so the qualifier is represented once and round-trips + * consistently across all contexts. + */ +public final class IntervalQualifier implements Serializable { + + private final String leadingField; + private final Integer leadingFieldPrecision; + private final String trailingField; + private final Integer fractionalSecondsPrecision; + + public IntervalQualifier( + String leadingField, Integer leadingFieldPrecision, String trailingField, + Integer fractionalSecondsPrecision) { + this.leadingField = leadingField; + this.leadingFieldPrecision = leadingFieldPrecision; + this.trailingField = trailingField; + this.fractionalSecondsPrecision = fractionalSecondsPrecision; + } + + public String getLeadingField() { + return leadingField; + } + + public Integer getLeadingFieldPrecision() { + return leadingFieldPrecision; + } + + public String getTrailingField() { + return trailingField; + } + + public Integer getFractionalSecondsPrecision() { + return fractionalSecondsPrecision; + } + + public IntervalQualifier withLeadingField(String leadingField) { + return new IntervalQualifier( + leadingField, leadingFieldPrecision, trailingField, fractionalSecondsPrecision); + } + + public IntervalQualifier withLeadingFieldPrecision(Integer leadingFieldPrecision) { + return new IntervalQualifier( + leadingField, leadingFieldPrecision, trailingField, fractionalSecondsPrecision); + } + + public IntervalQualifier withTrailingField(String trailingField) { + return new IntervalQualifier( + leadingField, leadingFieldPrecision, trailingField, fractionalSecondsPrecision); + } + + public IntervalQualifier withFractionalSecondsPrecision(Integer fractionalSecondsPrecision) { + return new IntervalQualifier( + leadingField, leadingFieldPrecision, trailingField, fractionalSecondsPrecision); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof IntervalQualifier)) { + return false; + } + IntervalQualifier that = (IntervalQualifier) o; + return Objects.equals(leadingField, that.leadingField) + && Objects.equals(leadingFieldPrecision, that.leadingFieldPrecision) + && Objects.equals(trailingField, that.trailingField) + && Objects.equals(fractionalSecondsPrecision, that.fractionalSecondsPrecision); + } + + @Override + public int hashCode() { + return Objects.hash( + leadingField, leadingFieldPrecision, trailingField, fractionalSecondsPrecision); + } + + /** + * Renders the qualifier back to SQL, e.g. {@code DAY}, {@code DAY TO SECOND}, + * {@code DAY(9) TO SECOND} or {@code SECOND(2, 4)}. + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(Objects.toString(leadingField, "")); + if (leadingFieldPrecision != null) { + sb.append("(").append(leadingFieldPrecision); + // For a single-field qualifier such as SECOND(2, 4), the fractional seconds + // precision is rendered together with the leading precision. + if (trailingField == null && fractionalSecondsPrecision != null) { + sb.append(", ").append(fractionalSecondsPrecision); + } + sb.append(")"); + } + if (trailingField != null) { + sb.append(" TO ").append(trailingField); + if (fractionalSecondsPrecision != null) { + sb.append("(").append(fractionalSecondsPrecision).append(")"); + } + } + return sb.toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonAggregateFunction.java b/src/main/java/net/sf/jsqlparser/expression/JsonAggregateFunction.java index 20bfa272e..a60e52b7f 100644 --- a/src/main/java/net/sf/jsqlparser/expression/JsonAggregateFunction.java +++ b/src/main/java/net/sf/jsqlparser/expression/JsonAggregateFunction.java @@ -10,6 +10,7 @@ package net.sf.jsqlparser.expression; import java.util.List; +import java.util.Locale; import java.util.Objects; import net.sf.jsqlparser.statement.select.OrderByElement; @@ -73,7 +74,7 @@ public void setType(String typeName) { .valueOf(Objects .requireNonNull(typeName, "The Type of the JSON Aggregate Function must not be null") - .toUpperCase()); + .toUpperCase(Locale.ROOT)); } public JsonAggregateFunction withType(JsonFunctionType type) { diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonAggregateOnNullType.java b/src/main/java/net/sf/jsqlparser/expression/JsonAggregateOnNullType.java index 898dad7c0..0cfcf4e02 100644 --- a/src/main/java/net/sf/jsqlparser/expression/JsonAggregateOnNullType.java +++ b/src/main/java/net/sf/jsqlparser/expression/JsonAggregateOnNullType.java @@ -25,6 +25,8 @@ package net.sf.jsqlparser.expression; +import java.util.Locale; + /** * @author Andreas Reichel */ @@ -32,6 +34,6 @@ public enum JsonAggregateOnNullType { NULL, ABSENT; public static JsonAggregateOnNullType from(String type) { - return Enum.valueOf(JsonAggregateOnNullType.class, type.toUpperCase()); + return Enum.valueOf(JsonAggregateOnNullType.class, type.toUpperCase(Locale.ROOT)); } } diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonAggregateUniqueKeysType.java b/src/main/java/net/sf/jsqlparser/expression/JsonAggregateUniqueKeysType.java index 097aad552..b1c1cd235 100644 --- a/src/main/java/net/sf/jsqlparser/expression/JsonAggregateUniqueKeysType.java +++ b/src/main/java/net/sf/jsqlparser/expression/JsonAggregateUniqueKeysType.java @@ -25,6 +25,8 @@ package net.sf.jsqlparser.expression; +import java.util.Locale; + /** * @author Andreas Reichel */ @@ -32,6 +34,6 @@ public enum JsonAggregateUniqueKeysType { WITH, WITHOUT; public static JsonAggregateUniqueKeysType from(String type) { - return Enum.valueOf(JsonAggregateUniqueKeysType.class, type.toUpperCase()); + return Enum.valueOf(JsonAggregateUniqueKeysType.class, type.toUpperCase(Locale.ROOT)); } } diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonExpression.java b/src/main/java/net/sf/jsqlparser/expression/JsonExpression.java index f258e855c..7cd34b53d 100644 --- a/src/main/java/net/sf/jsqlparser/expression/JsonExpression.java +++ b/src/main/java/net/sf/jsqlparser/expression/JsonExpression.java @@ -87,7 +87,10 @@ public String toString() { StringBuilder b = new StringBuilder(); b.append(expr.toString()); for (Map.Entry ident : idents) { - b.append(ident.getValue()).append(ident.getKey()); + // Separate the operator with spaces: without them `#>`/`#>>` glue onto + // the operand (e.g. `a#>'{b}'`), and since `#` is a legal identifier + // character the result re-parses as `a#` `>` `...` -- a different tree. + b.append(' ').append(ident.getValue()).append(' ').append(ident.getKey()); } return b.toString(); } diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java b/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java index 4422c1beb..dfad9451a 100644 --- a/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java +++ b/src/main/java/net/sf/jsqlparser/expression/JsonFunction.java @@ -10,25 +10,160 @@ package net.sf.jsqlparser.expression; import java.util.ArrayList; +import java.util.Locale; import java.util.Objects; import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.statement.create.table.ColDataType; /** + * Represents a JSON-Function.
+ * Currently supported are the types in {@link JsonFunctionType}.
+ *
+ * For JSON_OBJECT the parameters are available from {@link #getKeyValuePairs()}
+ *
+ * For JSON_ARRAY the parameters are availble from {@link #getExpressions()}.
+ * * @author Andreas Reichel */ - public class JsonFunction extends ASTNodeAccessImpl implements Expression { + public enum JsonOnResponseBehaviorType { + ERROR, NULL, DEFAULT, EMPTY, EMPTY_ARRAY, EMPTY_OBJECT, TRUE, FALSE, UNKNOWN + } + + public enum JsonWrapperType { + WITHOUT, WITH + } + + public enum JsonWrapperMode { + CONDITIONAL, UNCONDITIONAL + } + + public enum JsonQuotesType { + KEEP, OMIT + } + + public enum ScalarsType { + ALLOW, DISALLOW + } + + public static class JsonOnResponseBehavior { + private JsonOnResponseBehaviorType type; + private Expression expression; + + public JsonOnResponseBehavior(JsonOnResponseBehaviorType type) { + this(type, null); + } + + public JsonOnResponseBehavior(JsonOnResponseBehaviorType type, Expression expression) { + this.type = type; + this.expression = expression; + } + + public JsonOnResponseBehaviorType getType() { + return type; + } + + public void setType(JsonOnResponseBehaviorType type) { + this.type = type; + } + + public Expression getExpression() { + return expression; + } + + public void setExpression(Expression expression) { + this.expression = expression; + } + + public StringBuilder append(StringBuilder builder) { + switch (type) { + case ERROR: + builder.append("ERROR"); + break; + case NULL: + builder.append("NULL"); + break; + case DEFAULT: + builder.append("DEFAULT ").append(expression); + break; + case EMPTY: + builder.append("EMPTY "); + break; + case EMPTY_ARRAY: + builder.append("EMPTY ARRAY"); + break; + case EMPTY_OBJECT: + builder.append("EMPTY OBJECT"); + break; + case TRUE: + builder.append("TRUE"); + break; + case FALSE: + builder.append("FALSE"); + break; + case UNKNOWN: + builder.append("UNKNOWN"); + break; + default: + throw new IllegalStateException("Unhandled JsonOnResponseBehavior: " + type); + // this should never happen + } + return builder; + } + + @Override + public String toString() { + return append(new StringBuilder()).toString(); + } + } + private final ArrayList keyValuePairs = new ArrayList<>(); private final ArrayList expressions = new ArrayList<>(); + private final ArrayList passingExpressions = new ArrayList<>(); + private final ArrayList additionalQueryPathArguments = new ArrayList<>(); private JsonFunctionType functionType; private JsonAggregateOnNullType onNullType; private JsonAggregateUniqueKeysType uniqueKeysType; + private boolean isStrict = false; + private JsonFunctionExpression inputExpression; + private Expression jsonPathExpression; + private ColDataType returningType; + private boolean returningFormatJson; + private String returningEncoding; + private JsonOnResponseBehavior onEmptyBehavior; + private JsonOnResponseBehavior onErrorBehavior; + private JsonWrapperType wrapperType; + private JsonWrapperMode wrapperMode; + private boolean wrapperArray; + private JsonQuotesType quotesType; + private boolean quotesOnScalarString; + private ScalarsType scalarsType; + + public JsonFunction() {} + + public JsonFunction(JsonFunctionType functionType) { + this.functionType = functionType; + } + + /** + * Returns the Parameters of an JSON_OBJECT
+ * The KeyValuePairs may not have both key and value set, in some cases only the Key is set. + * + * @see net.sf.jsqlparser.parser.feature.Feature#allowCommaAsKeyValueSeparator + * + * @return A List of KeyValuePairs, never NULL + */ public ArrayList getKeyValuePairs() { return keyValuePairs; } + /** + * Returns the parameters of JSON_ARRAY
+ * + * @return A List of {@link JsonFunctionExpression}s, never NULL + */ public ArrayList getExpressions() { return expressions; } @@ -57,6 +192,126 @@ public void add(int i, JsonFunctionExpression expression) { expressions.add(i, expression); } + public ArrayList getPassingExpressions() { + return passingExpressions; + } + + public boolean addPassingExpression(Expression expression) { + return passingExpressions.add(expression); + } + + public ArrayList getAdditionalQueryPathArguments() { + return additionalQueryPathArguments; + } + + public boolean addAdditionalQueryPathArgument(String argument) { + return additionalQueryPathArguments.add(argument); + } + + public JsonFunctionExpression getInputExpression() { + return inputExpression; + } + + public void setInputExpression(JsonFunctionExpression inputExpression) { + this.inputExpression = inputExpression; + } + + public Expression getJsonPathExpression() { + return jsonPathExpression; + } + + public void setJsonPathExpression(Expression jsonPathExpression) { + this.jsonPathExpression = jsonPathExpression; + } + + public ColDataType getReturningType() { + return returningType; + } + + public void setReturningType(ColDataType returningType) { + this.returningType = returningType; + } + + public boolean isReturningFormatJson() { + return returningFormatJson; + } + + public void setReturningFormatJson(boolean returningFormatJson) { + this.returningFormatJson = returningFormatJson; + } + + public String getReturningEncoding() { + return returningEncoding; + } + + public void setReturningEncoding(String returningEncoding) { + this.returningEncoding = returningEncoding; + } + + public JsonOnResponseBehavior getOnEmptyBehavior() { + return onEmptyBehavior; + } + + public void setOnEmptyBehavior(JsonOnResponseBehavior onEmptyBehavior) { + this.onEmptyBehavior = onEmptyBehavior; + } + + public JsonOnResponseBehavior getOnErrorBehavior() { + return onErrorBehavior; + } + + public void setOnErrorBehavior(JsonOnResponseBehavior onErrorBehavior) { + this.onErrorBehavior = onErrorBehavior; + } + + public JsonWrapperType getWrapperType() { + return wrapperType; + } + + public void setWrapperType(JsonWrapperType wrapperType) { + this.wrapperType = wrapperType; + } + + public JsonWrapperMode getWrapperMode() { + return wrapperMode; + } + + public void setWrapperMode(JsonWrapperMode wrapperMode) { + this.wrapperMode = wrapperMode; + } + + public boolean isWrapperArray() { + return wrapperArray; + } + + public void setWrapperArray(boolean wrapperArray) { + this.wrapperArray = wrapperArray; + } + + public JsonQuotesType getQuotesType() { + return quotesType; + } + + public void setQuotesType(JsonQuotesType quotesType) { + this.quotesType = quotesType; + } + + public boolean isQuotesOnScalarString() { + return quotesOnScalarString; + } + + public void setQuotesOnScalarString(boolean quotesOnScalarString) { + this.quotesOnScalarString = quotesOnScalarString; + } + + public ScalarsType getScalarsType() { + return scalarsType; + } + + public void setScalarsType(ScalarsType type) { + this.scalarsType = type; + } + public boolean isEmpty() { return keyValuePairs.isEmpty(); } @@ -101,7 +356,7 @@ public void setType(String typeName) { this.functionType = JsonFunctionType.valueOf( Objects.requireNonNull(typeName, "The Type of the JSON Aggregate Function must not be null") - .toUpperCase()); + .toUpperCase(Locale.ROOT)); } public JsonFunction withType(JsonFunctionType type) { @@ -114,6 +369,19 @@ public JsonFunction withType(String typeName) { return this; } + public boolean isStrict() { + return isStrict; + } + + public void setStrict(boolean strict) { + isStrict = strict; + } + + public JsonFunction withStrict(boolean strict) { + this.setStrict(strict); + return this; + } + @Override public T accept(ExpressionVisitor expressionVisitor, S context) { return expressionVisitor.visit(this, context); @@ -123,17 +391,22 @@ public T accept(ExpressionVisitor expressionVisitor, S context) { public StringBuilder append(StringBuilder builder) { switch (functionType) { case OBJECT: - appendObject(builder); - break; case POSTGRES_OBJECT: - appendPostgresObject(builder); - break; case MYSQL_OBJECT: - appendMySqlObject(builder); + appendObject(builder); break; case ARRAY: appendArray(builder); break; + case VALUE: + appendValue(builder); + break; + case QUERY: + appendQuery(builder); + break; + case EXISTS: + appendExists(builder); + break; default: // this should never happen really } @@ -148,35 +421,38 @@ public StringBuilder appendObject(StringBuilder builder) { if (i > 0) { builder.append(", "); } - if (keyValuePair.isUsingValueKeyword()) { - if (keyValuePair.isUsingKeyKeyword()) { - builder.append("KEY "); - } - builder.append(keyValuePair.getKey()).append(" VALUE ") - .append(keyValuePair.getValue()); - } else { - builder.append(keyValuePair.getKey()).append(":").append(keyValuePair.getValue()); - } - - if (keyValuePair.isUsingFormatJson()) { - builder.append(" FORMAT JSON"); - } + keyValuePair.append(builder); i++; } + appendOnNullType(builder); + if (isStrict) { + builder.append(" STRICT"); + } + appendUniqueKeys(builder); + appendReturningClause(builder, true); + + builder.append(" ) "); + + return builder; + } + + private void appendOnNullType(StringBuilder builder) { if (onNullType != null) { switch (onNullType) { case NULL: builder.append(" NULL ON NULL"); break; case ABSENT: - builder.append(" ABSENT On NULL"); + builder.append(" ABSENT ON NULL"); break; default: // this should never happen } } + } + private void appendUniqueKeys(StringBuilder builder) { if (uniqueKeysType != null) { switch (uniqueKeysType) { case WITH: @@ -189,71 +465,140 @@ public StringBuilder appendObject(StringBuilder builder) { // this should never happen } } + } - builder.append(" ) "); + @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"}) + public StringBuilder appendArray(StringBuilder builder) { + builder.append("JSON_ARRAY( "); + int i = 0; + + for (JsonFunctionExpression expr : expressions) { + if (i > 0) { + builder.append(", "); + } + expr.append(builder); + i++; + } + + appendOnNullType(builder); + appendReturningClause(builder, true); + builder.append(") "); return builder; } - @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"}) - public StringBuilder appendPostgresObject(StringBuilder builder) { - builder.append("JSON_OBJECT( "); - for (JsonKeyValuePair keyValuePair : keyValuePairs) { - builder.append(keyValuePair.getKey()); - if (keyValuePair.getValue() != null) { - builder.append(", ").append(keyValuePair.getValue()); - } + public StringBuilder appendValue(StringBuilder builder) { + builder.append("JSON_VALUE("); + appendValueOrQueryPrefix(builder); + + if (returningType != null) { + builder.append(" RETURNING ").append(returningType); } - builder.append(" ) "); + appendOnResponseClause(builder, onEmptyBehavior, "EMPTY"); + appendOnResponseClause(builder, onErrorBehavior, "ERROR"); + + builder.append(")"); return builder; } - public StringBuilder appendMySqlObject(StringBuilder builder) { - builder.append("JSON_OBJECT( "); - int i = 0; - for (JsonKeyValuePair keyValuePair : keyValuePairs) { - if (i > 0) { - builder.append(", "); - } - builder.append(keyValuePair.getKey()); - builder.append(", ").append(keyValuePair.getValue()); - i++; + @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"}) + public StringBuilder appendQuery(StringBuilder builder) { + builder.append("JSON_QUERY("); + appendValueOrQueryPrefix(builder); + + appendReturningClause(builder, true); + + appendWrapperClause(builder); + appendQuotesClause(builder); + appendOnResponseClause(builder, onEmptyBehavior, "EMPTY"); + appendOnResponseClause(builder, onErrorBehavior, "ERROR"); + + for (String additionalQueryPathArgument : additionalQueryPathArguments) { + builder.append(", ").append(additionalQueryPathArgument); } - builder.append(" ) "); + builder.append(")"); return builder; } @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.NPathComplexity"}) - public StringBuilder appendArray(StringBuilder builder) { - builder.append("JSON_ARRAY( "); - int i = 0; + public StringBuilder appendExists(StringBuilder builder) { + builder.append("JSON_EXISTS("); + appendValueOrQueryPrefix(builder); + appendOnResponseClause(builder, onErrorBehavior, "ERROR"); + builder.append(")"); + return builder; + } - for (JsonFunctionExpression expr : expressions) { - if (i > 0) { + private void appendValueOrQueryPrefix(StringBuilder builder) { + if (inputExpression != null) { + inputExpression.append(builder); + } + + if (jsonPathExpression != null) { + if (inputExpression != null) { builder.append(", "); } - expr.append(builder); - i++; + builder.append(jsonPathExpression); } - if (onNullType != null) { - switch (onNullType) { - case NULL: - builder.append(" NULL ON NULL "); - break; - case ABSENT: - builder.append(" ABSENT ON NULL "); - break; - default: - // "ON NULL" was omitted + if (!passingExpressions.isEmpty()) { + builder.append(" PASSING "); + boolean comma = false; + for (Expression passingExpression : passingExpressions) { + if (comma) { + builder.append(", "); + } else { + comma = true; + } + builder.append(passingExpression); } } - builder.append(") "); + } - return builder; + private void appendOnResponseClause(StringBuilder builder, JsonOnResponseBehavior behavior, + String clause) { + if (behavior != null) { + builder.append(" "); + behavior.append(builder); + builder.append(" ON ").append(clause); + } + } + + private void appendReturningClause(StringBuilder builder, boolean formatJsonAllowed) { + if (returningType != null) { + builder.append(" RETURNING ").append(returningType); + if (formatJsonAllowed && returningFormatJson) { + builder.append(" FORMAT JSON"); + if (returningEncoding != null) { + builder.append(" ENCODING ").append(returningEncoding); + } + } + } + } + + private void appendWrapperClause(StringBuilder builder) { + if (wrapperType != null) { + builder.append(" ").append(wrapperType); + if (wrapperMode != null) { + builder.append(" ").append(wrapperMode); + } + if (wrapperArray) { + builder.append(" ARRAY"); + } + builder.append(" WRAPPER"); + } + } + + private void appendQuotesClause(StringBuilder builder) { + if (quotesType != null) { + builder.append(" ").append(quotesType).append(" QUOTES"); + if (quotesOnScalarString) { + builder.append(" ON SCALAR STRING"); + } + } } @Override diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonFunctionExpression.java b/src/main/java/net/sf/jsqlparser/expression/JsonFunctionExpression.java index 5df7ad310..738c09fc2 100644 --- a/src/main/java/net/sf/jsqlparser/expression/JsonFunctionExpression.java +++ b/src/main/java/net/sf/jsqlparser/expression/JsonFunctionExpression.java @@ -21,6 +21,7 @@ public class JsonFunctionExpression implements Serializable { private final Expression expression; private boolean usingFormatJson = false; + private String encoding; public JsonFunctionExpression(Expression expression) { this.expression = Objects.requireNonNull(expression, "The EXPRESSION must not be null"); @@ -43,8 +44,28 @@ public JsonFunctionExpression withUsingFormatJson(boolean usingFormatJson) { return this; } + public String getEncoding() { + return encoding; + } + + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + public JsonFunctionExpression withEncoding(String encoding) { + this.setEncoding(encoding); + return this; + } + public StringBuilder append(StringBuilder builder) { - return builder.append(getExpression()).append(isUsingFormatJson() ? " FORMAT JSON" : ""); + builder.append(getExpression()); + if (isUsingFormatJson()) { + builder.append(" FORMAT JSON"); + if (encoding != null) { + builder.append(" ENCODING ").append(encoding); + } + } + return builder; } @Override diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonFunctionType.java b/src/main/java/net/sf/jsqlparser/expression/JsonFunctionType.java index 43a33aab6..b9889d1ea 100644 --- a/src/main/java/net/sf/jsqlparser/expression/JsonFunctionType.java +++ b/src/main/java/net/sf/jsqlparser/expression/JsonFunctionType.java @@ -10,13 +10,27 @@ package net.sf.jsqlparser.expression; +import java.util.Locale; + /** * @author Andreas Reichel */ public enum JsonFunctionType { - OBJECT, ARRAY, POSTGRES_OBJECT, MYSQL_OBJECT; + OBJECT, ARRAY, VALUE, QUERY, EXISTS, + + /** + * Not used anymore + */ + @Deprecated + POSTGRES_OBJECT, + + /** + * Not used anymore + */ + @Deprecated + MYSQL_OBJECT; public static JsonFunctionType from(String type) { - return Enum.valueOf(JsonFunctionType.class, type.toUpperCase()); + return Enum.valueOf(JsonFunctionType.class, type.toUpperCase(Locale.ROOT)); } } diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePair.java b/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePair.java index 82c8a355a..18fb4752d 100644 --- a/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePair.java +++ b/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePair.java @@ -20,16 +20,28 @@ public class JsonKeyValuePair implements Serializable { private final Object key; private final Object value; - private boolean usingKeyKeyword = false; - private boolean usingValueKeyword = false; + private boolean usingKeyKeyword; + private JsonKeyValuePairSeparator separator; private boolean usingFormatJson = false; + private String encoding; + /** + * Please use the Constructor with {@link JsonKeyValuePairSeparator} parameter. + */ + @Deprecated public JsonKeyValuePair(Object key, Object value, boolean usingKeyKeyword, boolean usingValueKeyword) { + this(key, value, usingKeyKeyword, usingValueKeyword ? JsonKeyValuePairSeparator.VALUE + : JsonKeyValuePairSeparator.COLON); + } + + public JsonKeyValuePair(Object key, Object value, boolean usingKeyKeyword, + JsonKeyValuePairSeparator separator) { this.key = Objects.requireNonNull(key, "The KEY of the Pair must not be null"); this.value = value; this.usingKeyKeyword = usingKeyKeyword; - this.usingValueKeyword = usingValueKeyword; + this.separator = + Objects.requireNonNull(separator, "The KeyValuePairSeparator must not be NULL"); } public boolean isUsingKeyKeyword() { @@ -45,19 +57,45 @@ public JsonKeyValuePair withUsingKeyKeyword(boolean usingKeyKeyword) { return this; } + /** + * Use {@link #getSeparator()} + */ + @Deprecated public boolean isUsingValueKeyword() { - return usingValueKeyword; + return separator == JsonKeyValuePairSeparator.VALUE; } + /** + * Use {@link #setSeparator(JsonKeyValuePairSeparator)} + */ + @Deprecated public void setUsingValueKeyword(boolean usingValueKeyword) { - this.usingValueKeyword = usingValueKeyword; + separator = usingValueKeyword ? JsonKeyValuePairSeparator.VALUE + : JsonKeyValuePairSeparator.COLON; } + /** + * Use {@link #withSeparator(JsonKeyValuePairSeparator)} + */ + @Deprecated public JsonKeyValuePair withUsingValueKeyword(boolean usingValueKeyword) { this.setUsingValueKeyword(usingValueKeyword); return this; } + public JsonKeyValuePairSeparator getSeparator() { + return separator; + } + + public void setSeparator(JsonKeyValuePairSeparator separator) { + this.separator = separator; + } + + public JsonKeyValuePair withSeparator(JsonKeyValuePairSeparator separator) { + this.setSeparator(separator); + return this; + } + public boolean isUsingFormatJson() { return usingFormatJson; } @@ -71,6 +109,19 @@ public JsonKeyValuePair withUsingFormatJson(boolean usingFormatJson) { return this; } + public String getEncoding() { + return encoding; + } + + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + public JsonKeyValuePair withEncoding(String encoding) { + this.setEncoding(encoding); + return this; + } + @Override public int hashCode() { int hash = 7; @@ -102,17 +153,21 @@ public Object getValue() { } public StringBuilder append(StringBuilder builder) { - if (isUsingValueKeyword()) { - if (isUsingKeyKeyword()) { - builder.append("KEY "); - } - builder.append(getKey()).append(" VALUE ").append(getValue()); - } else { - builder.append(getKey()).append(":").append(getValue()); + if (isUsingKeyKeyword() && getSeparator() == JsonKeyValuePairSeparator.VALUE) { + builder.append("KEY "); + } + builder.append(getKey()); + + if (getValue() != null) { + builder.append(getSeparator().getSeparatorString()); + builder.append(getValue()); } if (isUsingFormatJson()) { builder.append(" FORMAT JSON"); + if (encoding != null) { + builder.append(" ENCODING ").append(encoding); + } } return builder; diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePairSeparator.java b/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePairSeparator.java new file mode 100644 index 000000000..e4e998aa5 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/expression/JsonKeyValuePairSeparator.java @@ -0,0 +1,33 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2025 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.expression; + +/** + * Describes the string used to separate the key from the value. + */ +public enum JsonKeyValuePairSeparator { + VALUE(" VALUE "), COLON(":"), + + // Used in MySQL dialect + COMMA(","), + + // Is used in case they KeyValuePair has only a key and no value + NOT_USED(""); + + private final String separator; + + JsonKeyValuePairSeparator(String separator) { + this.separator = separator; + } + + public String getSeparatorString() { + return separator; + } +} diff --git a/src/main/java/net/sf/jsqlparser/expression/JsonTableFunction.java b/src/main/java/net/sf/jsqlparser/expression/JsonTableFunction.java new file mode 100644 index 000000000..5ee166e6f --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/expression/JsonTableFunction.java @@ -0,0 +1,864 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.expression; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.statement.create.table.ColDataType; + +public class JsonTableFunction extends Function { + + private Expression jsonInputExpression; + private Expression jsonPathExpression; + private String pathName; + private final List passingClauses = new ArrayList<>(); + private JsonTableColumnsClause columnsClause; + private JsonTablePlanClause planClause; + private JsonTableOnErrorClause onErrorClause; + private JsonTableParsingTypeClause parsingTypeClause; + private JsonTableOnEmptyClause onEmptyClause; + private boolean formatJson; + + public enum JsonTablePlanOperator { + COMMA(", "), INNER(" INNER "), OUTER(" OUTER "), CROSS(" CROSS "), UNION(" UNION "); + + private final String display; + + JsonTablePlanOperator(String display) { + this.display = display; + } + + public String getDisplay() { + return display; + } + } + + public enum JsonTableOnErrorType { + ERROR, NULL, EMPTY, TRUE, FALSE + } + + public enum JsonTableOnEmptyType { + ERROR, NULL, EMPTY, TRUE, FALSE + } + + public enum JsonTableParsingType { + STRICT, LAX + } + + public static class JsonTablePassingClause extends ASTNodeAccessImpl implements Serializable { + private Expression valueExpression; + private String parameterName; + + public JsonTablePassingClause() {} + + public JsonTablePassingClause(Expression valueExpression, String parameterName) { + this.valueExpression = valueExpression; + this.parameterName = parameterName; + } + + public Expression getValueExpression() { + return valueExpression; + } + + public JsonTablePassingClause setValueExpression(Expression valueExpression) { + this.valueExpression = valueExpression; + return this; + } + + public String getParameterName() { + return parameterName; + } + + public JsonTablePassingClause setParameterName(String parameterName) { + this.parameterName = parameterName; + return this; + } + + public void collectExpressions(List expressions) { + if (valueExpression != null) { + expressions.add(valueExpression); + } + } + + @Override + public String toString() { + return valueExpression + " AS " + parameterName; + } + } + + public static class JsonTableWrapperClause extends ASTNodeAccessImpl implements Serializable { + private boolean beforePathExpression; + private JsonFunction.JsonWrapperType wrapperType; + private JsonFunction.JsonWrapperMode wrapperMode; + private boolean array; + + /** + * Creates a wrapper clause. Depending on the dialect, this clause can come before or after + * the PATH expression. + *

    + *
  • Trino: after PATH
  • + *
  • Oracle: before PATH
  • + *
+ * + * @param beforePathExpression A flag to determine wether the clause is rendered before or + * after the PATH expression + */ + public JsonTableWrapperClause(boolean beforePathExpression) { + this.beforePathExpression = beforePathExpression; + } + + public boolean isBeforePathExpression() { + return beforePathExpression; + } + + public JsonFunction.JsonWrapperType getWrapperType() { + return wrapperType; + } + + public JsonTableWrapperClause setWrapperType(JsonFunction.JsonWrapperType wrapperType) { + this.wrapperType = wrapperType; + return this; + } + + public JsonFunction.JsonWrapperMode getWrapperMode() { + return wrapperMode; + } + + public JsonTableWrapperClause setWrapperMode(JsonFunction.JsonWrapperMode wrapperMode) { + this.wrapperMode = wrapperMode; + return this; + } + + public boolean isArray() { + return array; + } + + public JsonTableWrapperClause setArray(boolean array) { + this.array = array; + return this; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append(wrapperType); + if (wrapperMode != null) { + builder.append(" ").append(wrapperMode); + } + if (array) { + builder.append(" ARRAY"); + } + builder.append(" WRAPPER"); + return builder.toString(); + } + } + + public static class JsonTableQuotesClause extends ASTNodeAccessImpl implements Serializable { + private JsonFunction.JsonQuotesType quotesType; + private boolean onScalarString; + + public JsonFunction.JsonQuotesType getQuotesType() { + return quotesType; + } + + public JsonTableQuotesClause setQuotesType(JsonFunction.JsonQuotesType quotesType) { + this.quotesType = quotesType; + return this; + } + + public boolean isOnScalarString() { + return onScalarString; + } + + public JsonTableQuotesClause setOnScalarString(boolean onScalarString) { + this.onScalarString = onScalarString; + return this; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append(quotesType).append(" QUOTES"); + if (onScalarString) { + builder.append(" ON SCALAR STRING"); + } + return builder.toString(); + } + } + + public static class JsonTableOnErrorClause extends ASTNodeAccessImpl implements Serializable { + private JsonTableOnErrorType type; + private boolean beforeColumns = true; + + public JsonTableOnErrorClause(boolean beforeColumns) { + this.beforeColumns = beforeColumns; + } + + public boolean isBeforeColumns() { + return beforeColumns; + } + + public JsonTableOnErrorType getType() { + return type; + } + + public JsonTableOnErrorClause setType(JsonTableOnErrorType type) { + this.type = type; + return this; + } + + @Override + public String toString() { + return type + " ON ERROR"; + } + } + + public static class JsonTableOnEmptyClause extends ASTNodeAccessImpl implements Serializable { + private JsonTableOnEmptyType type; + + public JsonTableOnEmptyType getType() { + return type; + } + + public JsonTableOnEmptyClause setType(JsonTableOnEmptyType type) { + this.type = type; + return this; + } + + @Override + public String toString() { + return type + " ON EMPTY"; + } + } + + public static class JsonTableParsingTypeClause extends ASTNodeAccessImpl + implements Serializable { + private JsonTableParsingType type; + + public JsonTableParsingType getType() { + return type; + } + + public JsonTableParsingTypeClause setType(JsonTableParsingType type) { + this.type = type; + return this; + } + + @Override + public String toString() { + return "TYPE(" + type + ")"; + } + } + + public static class JsonTablePlanTerm extends ASTNodeAccessImpl implements Serializable { + private JsonTablePlanExpression nestedPlanExpression; + private String name; + private Expression expression; + + public JsonTablePlanExpression getNestedPlanExpression() { + return nestedPlanExpression; + } + + public JsonTablePlanTerm setNestedPlanExpression( + JsonTablePlanExpression nestedPlanExpression) { + this.nestedPlanExpression = nestedPlanExpression; + return this; + } + + public String getName() { + return name; + } + + public JsonTablePlanTerm setName(String name) { + this.name = name; + return this; + } + + public Expression getExpression() { + return expression; + } + + public JsonTablePlanTerm setExpression(Expression expression) { + this.expression = expression; + return this; + } + + public void collectExpressions(List expressions) { + if (expression != null) { + expressions.add(expression); + } + if (nestedPlanExpression != null) { + nestedPlanExpression.collectExpressions(expressions); + } + } + + @Override + public String toString() { + if (nestedPlanExpression != null) { + return "(" + nestedPlanExpression + ")"; + } + if (name != null) { + return name; + } + return expression != null ? expression.toString() : ""; + } + } + + public static class JsonTablePlanExpression extends ASTNodeAccessImpl implements Serializable { + private final List terms = new ArrayList<>(); + private final List operators = new ArrayList<>(); + + public List getTerms() { + return terms; + } + + public JsonTablePlanExpression addTerm(JsonTablePlanTerm term) { + terms.add(term); + return this; + } + + public List getOperators() { + return operators; + } + + public JsonTablePlanExpression addOperator(JsonTablePlanOperator operator) { + operators.add(operator); + return this; + } + + public void collectExpressions(List expressions) { + for (JsonTablePlanTerm term : terms) { + if (term != null) { + term.collectExpressions(expressions); + } + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + if (!terms.isEmpty()) { + builder.append(terms.get(0)); + } + for (int i = 0; i < operators.size() && i + 1 < terms.size(); i++) { + builder.append(operators.get(i).getDisplay()).append(terms.get(i + 1)); + } + return builder.toString(); + } + } + + public static class JsonTablePlanClause extends ASTNodeAccessImpl implements Serializable { + private boolean defaultPlan; + private JsonTablePlanExpression planExpression; + + public boolean isDefaultPlan() { + return defaultPlan; + } + + public JsonTablePlanClause setDefaultPlan(boolean defaultPlan) { + this.defaultPlan = defaultPlan; + return this; + } + + public JsonTablePlanExpression getPlanExpression() { + return planExpression; + } + + public JsonTablePlanClause setPlanExpression(JsonTablePlanExpression planExpression) { + this.planExpression = planExpression; + return this; + } + + public void collectExpressions(List expressions) { + if (planExpression != null) { + planExpression.collectExpressions(expressions); + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("PLAN"); + if (defaultPlan) { + builder.append(" DEFAULT"); + } + builder.append(" (").append(planExpression).append(")"); + return builder.toString(); + } + } + + public abstract static class JsonTableColumnDefinition extends ASTNodeAccessImpl + implements Serializable { + public abstract void collectExpressions(List expressions); + } + + public static class JsonTableNestedColumnDefinition extends JsonTableColumnDefinition { + private boolean pathKeyword; + private Expression pathExpression; + private String pathName; + private JsonTableColumnsClause columnsClause; + + public boolean isPathKeyword() { + return pathKeyword; + } + + public JsonTableNestedColumnDefinition setPathKeyword(boolean pathKeyword) { + this.pathKeyword = pathKeyword; + return this; + } + + public Expression getPathExpression() { + return pathExpression; + } + + public JsonTableNestedColumnDefinition setPathExpression(Expression pathExpression) { + this.pathExpression = pathExpression; + return this; + } + + public String getPathName() { + return pathName; + } + + public JsonTableNestedColumnDefinition setPathName(String pathName) { + this.pathName = pathName; + return this; + } + + public JsonTableColumnsClause getColumnsClause() { + return columnsClause; + } + + public JsonTableNestedColumnDefinition setColumnsClause( + JsonTableColumnsClause columnsClause) { + this.columnsClause = columnsClause; + return this; + } + + @Override + public void collectExpressions(List expressions) { + if (pathExpression != null) { + expressions.add(pathExpression); + } + if (columnsClause != null) { + columnsClause.collectExpressions(expressions); + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("NESTED"); + if (pathKeyword) { + builder.append(" PATH"); + } + builder.append(" ").append(pathExpression); + if (pathName != null) { + builder.append(" AS ").append(pathName); + } + builder.append(" ").append(columnsClause); + return builder.toString(); + } + } + + public static class JsonTableValueColumnDefinition extends JsonTableColumnDefinition { + private String columnName; + private boolean forOrdinality; + private ColDataType dataType; + private boolean formatJson; + private boolean exists; + private boolean onEmptyAfterOnError; + private String encoding; + private Expression pathExpression; + private JsonTableWrapperClause wrapperClause; + private JsonTableQuotesClause quotesClause; + private JsonFunction.JsonOnResponseBehavior onEmptyBehavior; + private JsonFunction.JsonOnResponseBehavior onErrorBehavior; + private JsonFunction.ScalarsType scalarsType; + + public String getColumnName() { + return columnName; + } + + public JsonTableValueColumnDefinition setColumnName(String columnName) { + this.columnName = columnName; + return this; + } + + public boolean isExists() { + return exists; + } + + public JsonTableValueColumnDefinition setExistsKeyword(boolean exists) { + this.exists = exists; + return this; + } + + public JsonTableValueColumnDefinition setOnEmptyAfterOnError(boolean b) { + this.onEmptyAfterOnError = b; + return this; + } + + public boolean isForOrdinality() { + return forOrdinality; + } + + public JsonTableValueColumnDefinition setForOrdinality(boolean forOrdinality) { + this.forOrdinality = forOrdinality; + return this; + } + + public ColDataType getDataType() { + return dataType; + } + + public JsonTableValueColumnDefinition setDataType(ColDataType dataType) { + this.dataType = dataType; + return this; + } + + public boolean isFormatJson() { + return formatJson; + } + + public JsonTableValueColumnDefinition setFormatJson(boolean formatJson) { + this.formatJson = formatJson; + return this; + } + + public String getEncoding() { + return encoding; + } + + public JsonTableValueColumnDefinition setEncoding(String encoding) { + this.encoding = encoding; + return this; + } + + public Expression getPathExpression() { + return pathExpression; + } + + public JsonTableValueColumnDefinition setPathExpression(Expression pathExpression) { + this.pathExpression = pathExpression; + return this; + } + + public JsonTableWrapperClause getWrapperClause() { + return wrapperClause; + } + + public JsonTableValueColumnDefinition setWrapperClause( + JsonTableWrapperClause wrapperClause) { + this.wrapperClause = wrapperClause; + return this; + } + + public JsonTableQuotesClause getQuotesClause() { + return quotesClause; + } + + public JsonTableValueColumnDefinition setQuotesClause(JsonTableQuotesClause quotesClause) { + this.quotesClause = quotesClause; + return this; + } + + public JsonFunction.JsonOnResponseBehavior getOnEmptyBehavior() { + return onEmptyBehavior; + } + + public JsonTableValueColumnDefinition setOnEmptyBehavior( + JsonFunction.JsonOnResponseBehavior onEmptyBehavior) { + this.onEmptyBehavior = onEmptyBehavior; + return this; + } + + public JsonFunction.JsonOnResponseBehavior getOnErrorBehavior() { + return onErrorBehavior; + } + + public JsonTableValueColumnDefinition setOnErrorBehavior( + JsonFunction.JsonOnResponseBehavior onErrorBehavior) { + this.onErrorBehavior = onErrorBehavior; + return this; + } + + public void setScalarsType(JsonFunction.ScalarsType scalarsType) { + this.scalarsType = scalarsType; + } + + public JsonFunction.ScalarsType getScalarsType() { + return scalarsType; + } + + @Override + public void collectExpressions(List expressions) { + if (pathExpression != null) { + expressions.add(pathExpression); + } + if (onEmptyBehavior != null && onEmptyBehavior.getExpression() != null) { + expressions.add(onEmptyBehavior.getExpression()); + } + if (onErrorBehavior != null && onErrorBehavior.getExpression() != null) { + expressions.add(onErrorBehavior.getExpression()); + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(columnName); + if (forOrdinality) { + builder.append(" FOR ORDINALITY"); + return builder.toString(); + } + if (exists) { + builder.append(" EXISTS"); + } + if (dataType != null) { + builder.append(" ").append(dataType); + } + if (formatJson) { + builder.append(" FORMAT JSON"); + if (encoding != null) { + builder.append(" ENCODING ").append(encoding); + } + } + if (scalarsType != null) { + builder.append(" "); + builder.append(scalarsType); + builder.append(" SCALARS"); + } + if (wrapperClause != null && wrapperClause.isBeforePathExpression()) { + builder.append(" ").append(wrapperClause); + } + if (pathExpression != null) { + builder.append(" PATH ").append(pathExpression); + } + if (wrapperClause != null && !wrapperClause.isBeforePathExpression()) { + builder.append(" ").append(wrapperClause); + } + if (quotesClause != null) { + builder.append(" ").append(quotesClause); + } + if (onEmptyBehavior != null && !onEmptyAfterOnError) { + builder.append(" ").append(onEmptyBehavior).append(" ON EMPTY"); + } + if (onErrorBehavior != null) { + builder.append(" ").append(onErrorBehavior).append(" ON ERROR"); + } + if (onEmptyBehavior != null && onEmptyAfterOnError) { + builder.append(" ").append(onEmptyBehavior).append(" ON EMPTY"); + } + return builder.toString(); + } + } + + public static class JsonTableColumnsClause extends ASTNodeAccessImpl implements Serializable { + private final List columnDefinitions = new ArrayList<>(); + + public List getColumnDefinitions() { + return columnDefinitions; + } + + public JsonTableColumnsClause addColumnDefinition( + JsonTableColumnDefinition columnDefinition) { + columnDefinitions.add(columnDefinition); + return this; + } + + public void collectExpressions(List expressions) { + for (JsonTableColumnDefinition columnDefinition : columnDefinitions) { + if (columnDefinition != null) { + columnDefinition.collectExpressions(expressions); + } + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("COLUMNS ("); + boolean first = true; + for (JsonTableColumnDefinition columnDefinition : columnDefinitions) { + if (!first) { + builder.append(", "); + } + builder.append(columnDefinition); + first = false; + } + builder.append(")"); + return builder.toString(); + } + } + + public JsonTableFunction() { + setName("JSON_TABLE"); + } + + public boolean getFormatJson() { + return formatJson; + } + + public JsonTableFunction setFormatJson(boolean formatJson) { + this.formatJson = formatJson; + return this; + } + + public Expression getJsonInputExpression() { + return jsonInputExpression; + } + + public JsonTableFunction setJsonInputExpression(Expression jsonInputExpression) { + this.jsonInputExpression = jsonInputExpression; + return this; + } + + public Expression getJsonPathExpression() { + return jsonPathExpression; + } + + public JsonTableFunction setJsonPathExpression(Expression jsonPathExpression) { + this.jsonPathExpression = jsonPathExpression; + return this; + } + + public String getPathName() { + return pathName; + } + + public JsonTableFunction setPathName(String pathName) { + this.pathName = pathName; + return this; + } + + public List getPassingClauses() { + return passingClauses; + } + + public JsonTableFunction addPassingClause(JsonTablePassingClause passingClause) { + passingClauses.add(Objects.requireNonNull(passingClause, "passingClause")); + return this; + } + + public JsonTableColumnsClause getColumnsClause() { + return columnsClause; + } + + public JsonTableFunction setColumnsClause(JsonTableColumnsClause columnsClause) { + this.columnsClause = columnsClause; + return this; + } + + public JsonTablePlanClause getPlanClause() { + return planClause; + } + + public JsonTableFunction setPlanClause(JsonTablePlanClause planClause) { + this.planClause = planClause; + return this; + } + + public JsonTableOnErrorClause getOnErrorClause() { + return onErrorClause; + } + + public JsonTableFunction setOnErrorClause(JsonTableOnErrorClause onErrorClause) { + this.onErrorClause = onErrorClause; + return this; + } + + public JsonTableParsingTypeClause getParsingTypeClause() { + return parsingTypeClause; + } + + public JsonTableFunction setParsingTypeClause(JsonTableParsingTypeClause parsingTypeClause) { + this.parsingTypeClause = parsingTypeClause; + return this; + } + + public JsonTableOnEmptyClause getOnEmptyClause() { + return onEmptyClause; + } + + public JsonTableFunction setOnEmptyClause(JsonTableOnEmptyClause onEmptyClause) { + this.onEmptyClause = onEmptyClause; + return this; + } + + public List getAllExpressions() { + List expressions = new ArrayList<>(); + if (jsonInputExpression != null) { + expressions.add(jsonInputExpression); + } + if (jsonPathExpression != null) { + expressions.add(jsonPathExpression); + } + for (JsonTablePassingClause passingClause : passingClauses) { + passingClause.collectExpressions(expressions); + } + if (columnsClause != null) { + columnsClause.collectExpressions(expressions); + } + if (planClause != null) { + planClause.collectExpressions(expressions); + } + return expressions; + } + + @Override + public T accept(ExpressionVisitor expressionVisitor, S context) { + return expressionVisitor.visit(this, context); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("JSON_TABLE("); + builder.append(jsonInputExpression); + if (formatJson) { + builder.append(" FORMAT JSON"); + } + if (jsonPathExpression != null) { + builder.append(", ").append(jsonPathExpression); + } + if (pathName != null) { + builder.append(" AS ").append(pathName); + } + if (!passingClauses.isEmpty()) { + builder.append(" PASSING "); + boolean first = true; + for (JsonTablePassingClause passingClause : passingClauses) { + if (!first) { + builder.append(", "); + } + builder.append(passingClause); + first = false; + } + } + if (onErrorClause != null && onErrorClause.isBeforeColumns()) { + builder.append(" ").append(onErrorClause); + } + if (parsingTypeClause != null) { + builder.append(" ").append(parsingTypeClause); + } + if (onEmptyClause != null) { + builder.append(" ").append(onEmptyClause); + } + builder.append(" ").append(columnsClause); + if (planClause != null) { + builder.append(" ").append(planClause); + } + if (onErrorClause != null && !onErrorClause.isBeforeColumns()) { + builder.append(" ").append(onErrorClause); + } + builder.append(")"); + return builder.toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/expression/KeyExpression.java b/src/main/java/net/sf/jsqlparser/expression/KeyExpression.java new file mode 100644 index 000000000..2bb063875 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/expression/KeyExpression.java @@ -0,0 +1,44 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.expression; + +import java.util.Objects; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; + +/** + * Dialect specific expression for constructs such as {@code KEY chain.entity}. + */ +public class KeyExpression extends ASTNodeAccessImpl implements Expression { + private final Expression expression; + + public KeyExpression(Expression expression) { + this.expression = Objects.requireNonNull(expression, + "The EXPRESSION of the KEY expression must not be null"); + } + + public Expression getExpression() { + return expression; + } + + @Override + public T accept(ExpressionVisitor expressionVisitor, S context) { + return expressionVisitor.visit(this, context); + } + + public StringBuilder appendTo(StringBuilder builder) { + builder.append("KEY ").append(expression); + return builder; + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/expression/MapExpression.java b/src/main/java/net/sf/jsqlparser/expression/MapExpression.java new file mode 100644 index 000000000..1e64a7db8 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/expression/MapExpression.java @@ -0,0 +1,79 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.expression; + +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; + +/** + * DuckDB's {@code MAP {key: value, ...}} literal expression. + * + * @see DuckDB MAP type + */ +public class MapExpression extends ASTNodeAccessImpl implements Expression { + + private List> entries = new ArrayList<>(); + + public MapExpression() {} + + public MapExpression(Collection> entries) { + this.entries.addAll(entries); + } + + public List> getEntries() { + return entries; + } + + public void setEntries(List> entries) { + this.entries = entries; + } + + public MapExpression withEntries(List> entries) { + setEntries(entries); + return this; + } + + public MapExpression addEntries( + Collection> entries) { + this.entries.addAll(entries); + return this; + } + + public MapExpression addEntry(Expression key, Expression value) { + entries.add(new AbstractMap.SimpleEntry<>(key, value)); + return this; + } + + public StringBuilder appendTo(StringBuilder builder) { + builder.append("MAP {"); + for (int i = 0; i < entries.size(); i++) { + if (i > 0) { + builder.append(", "); + } + Map.Entry entry = entries.get(i); + builder.append(entry.getKey()).append(": ").append(entry.getValue()); + } + return builder.append("}"); + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } + + @Override + public T accept(ExpressionVisitor expressionVisitor, S context) { + return expressionVisitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/expression/OracleHint.java b/src/main/java/net/sf/jsqlparser/expression/OracleHint.java index 35ba8ad3b..166e19e08 100644 --- a/src/main/java/net/sf/jsqlparser/expression/OracleHint.java +++ b/src/main/java/net/sf/jsqlparser/expression/OracleHint.java @@ -24,7 +24,7 @@ public class OracleHint extends ASTNodeAccessImpl implements Expression { private static final Pattern SINGLE_LINE = Pattern.compile("--\\+ *([^ ].*[^ ])"); private static final Pattern MULTI_LINE = - Pattern.compile("\\/\\*\\+ *([^ ].*[^ ]) *\\*+\\/", Pattern.MULTILINE | Pattern.DOTALL); + Pattern.compile("\\A/\\*\\+ *([^ ].*[^ ]) *\\*+/", Pattern.MULTILINE | Pattern.DOTALL); private String value; private boolean singleLine = false; diff --git a/src/main/java/net/sf/jsqlparser/expression/PostgresNamedFunctionParameter.java b/src/main/java/net/sf/jsqlparser/expression/PostgresNamedFunctionParameter.java new file mode 100644 index 000000000..573ad60f2 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/expression/PostgresNamedFunctionParameter.java @@ -0,0 +1,55 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2021 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.expression; + +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; + +import java.util.Objects; + +/** + * @author Andreas Reichel + */ +public class PostgresNamedFunctionParameter extends ASTNodeAccessImpl implements Expression { + private final String name; + private final Expression expression; + + public PostgresNamedFunctionParameter(String name, Expression expression) { + this.name = Objects.requireNonNull(name, + "The NAME of the PostgresNamedFunctionParameter must not be null."); + this.expression = Objects.requireNonNull(expression, + "The EXPRESSION of the PostgresNamedFunctionParameter must not be null."); + } + + public String getName() { + return name; + } + + public Expression getExpression() { + return expression; + } + + @Override + public T accept(ExpressionVisitor expressionVisitor, S context) { + return expressionVisitor.visit(this, context); + } + + public StringBuilder appendTo(StringBuilder builder) { + builder.append(name) + .append(" := ") + .append(expression); + + return builder; + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/expression/RawFunction.java b/src/main/java/net/sf/jsqlparser/expression/RawFunction.java new file mode 100644 index 000000000..1c2d5b874 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/expression/RawFunction.java @@ -0,0 +1,41 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.expression; + +/** + * Function with a raw argument body preserved as-is for deparsing. + */ +public class RawFunction extends Function { + private String rawArguments; + + public RawFunction() {} + + public RawFunction(String name, String rawArguments) { + setName(name); + this.rawArguments = rawArguments; + } + + public String getRawArguments() { + return rawArguments; + } + + public void setRawArguments(String rawArguments) { + this.rawArguments = rawArguments; + } + + @Override + public String toString() { + String name = getName(); + if (rawArguments == null) { + return name + "()"; + } + return name + "(" + rawArguments + ")"; + } +} diff --git a/src/main/java/net/sf/jsqlparser/expression/SpannerInterleaveIn.java b/src/main/java/net/sf/jsqlparser/expression/SpannerInterleaveIn.java index 0bf49925d..4dcb0aa7f 100644 --- a/src/main/java/net/sf/jsqlparser/expression/SpannerInterleaveIn.java +++ b/src/main/java/net/sf/jsqlparser/expression/SpannerInterleaveIn.java @@ -9,6 +9,7 @@ */ package net.sf.jsqlparser.expression; +import java.util.Locale; import net.sf.jsqlparser.schema.Table; import java.util.Collections; @@ -74,7 +75,7 @@ public enum OnDelete { CASCADE, NO_ACTION; public static OnDelete from(String action) { - return Enum.valueOf(OnDelete.class, action.toUpperCase()); + return Enum.valueOf(OnDelete.class, action.toUpperCase(Locale.ROOT)); } } } diff --git a/src/main/java/net/sf/jsqlparser/expression/TernaryExpression.java b/src/main/java/net/sf/jsqlparser/expression/TernaryExpression.java new file mode 100644 index 000000000..3d67d4ea3 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/expression/TernaryExpression.java @@ -0,0 +1,68 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2025 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.expression; + +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; + +/** + * The C-style ternary conditional operator {@code condition ? thenExpression : elseExpression}, + * supported for instance by ClickHouse as an alias for {@code if(condition, then, else)}. + */ +public class TernaryExpression extends ASTNodeAccessImpl implements Expression { + private Expression condition; + private Expression thenExpression; + private Expression elseExpression; + + public TernaryExpression() {} + + public TernaryExpression(Expression condition, Expression thenExpression, + Expression elseExpression) { + this.condition = condition; + this.thenExpression = thenExpression; + this.elseExpression = elseExpression; + } + + public Expression getCondition() { + return condition; + } + + public TernaryExpression setCondition(Expression condition) { + this.condition = condition; + return this; + } + + public Expression getThenExpression() { + return thenExpression; + } + + public TernaryExpression setThenExpression(Expression thenExpression) { + this.thenExpression = thenExpression; + return this; + } + + public Expression getElseExpression() { + return elseExpression; + } + + public TernaryExpression setElseExpression(Expression elseExpression) { + this.elseExpression = elseExpression; + return this; + } + + @Override + public String toString() { + return condition + " ? " + thenExpression + " : " + elseExpression; + } + + @Override + public T accept(ExpressionVisitor expressionVisitor, S context) { + return expressionVisitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/expression/TranscodingFunction.java b/src/main/java/net/sf/jsqlparser/expression/TranscodingFunction.java index 343579e29..355e86305 100644 --- a/src/main/java/net/sf/jsqlparser/expression/TranscodingFunction.java +++ b/src/main/java/net/sf/jsqlparser/expression/TranscodingFunction.java @@ -9,20 +9,39 @@ */ package net.sf.jsqlparser.expression; +import java.util.Locale; import net.sf.jsqlparser.parser.ASTNodeAccessImpl; import net.sf.jsqlparser.statement.create.table.ColDataType; +import java.util.Objects; + public class TranscodingFunction extends ASTNodeAccessImpl implements Expression { + private String keyword = "CONVERT"; private boolean isTranscodeStyle = true; private ColDataType colDataType; private Expression expression; private String transcodingName; + public TranscodingFunction(String keyword, Expression expression, String transcodingName) { + this.keyword = Objects.requireNonNullElse(keyword, "CONVERT").toUpperCase(Locale.ROOT); + this.expression = expression; + this.transcodingName = transcodingName; + } + public TranscodingFunction(Expression expression, String transcodingName) { this.expression = expression; this.transcodingName = transcodingName; } + public TranscodingFunction(String keyword, ColDataType colDataType, Expression expression, + String transcodingName) { + this.keyword = Objects.requireNonNullElse(keyword, "CONVERT").toUpperCase(Locale.ROOT); + this.colDataType = colDataType; + this.expression = expression; + this.transcodingName = transcodingName; + this.isTranscodeStyle = false; + } + public TranscodingFunction(ColDataType colDataType, Expression expression, String transcodingName) { this.colDataType = colDataType; @@ -35,6 +54,15 @@ public TranscodingFunction() { this(null, null); } + public String getKeyword() { + return keyword; + } + + public TranscodingFunction setKeyword(String keyword) { + this.keyword = Objects.requireNonNullElse(keyword, "CONVERT").toUpperCase(Locale.ROOT); + return this; + } + public Expression getExpression() { return expression; } @@ -87,14 +115,16 @@ public T accept(ExpressionVisitor expressionVisitor, S context) { public StringBuilder appendTo(StringBuilder builder) { if (isTranscodeStyle) { return builder - .append("CONVERT( ") + .append(keyword) + .append("( ") .append(expression) .append(" USING ") .append(transcodingName) .append(" )"); } else { return builder - .append("CONVERT( ") + .append(keyword) + .append("( ") .append(colDataType) .append(", ") .append(expression) diff --git a/src/main/java/net/sf/jsqlparser/expression/WindowElement.java b/src/main/java/net/sf/jsqlparser/expression/WindowElement.java index 97260ce97..01f77557c 100644 --- a/src/main/java/net/sf/jsqlparser/expression/WindowElement.java +++ b/src/main/java/net/sf/jsqlparser/expression/WindowElement.java @@ -10,12 +10,14 @@ package net.sf.jsqlparser.expression; import java.io.Serializable; +import java.util.Locale; public class WindowElement implements Serializable { private Type type; private WindowOffset offset; private WindowRange range; + private Exclusion exclusion; public Type getType() { return type; @@ -41,6 +43,14 @@ public void setRange(WindowRange range) { this.range = range; } + public Exclusion getExclusion() { + return exclusion; + } + + public void setExclusion(Exclusion exclusion) { + this.exclusion = exclusion; + } + @Override public String toString() { StringBuilder buffer = new StringBuilder(type.toString()); @@ -51,6 +61,10 @@ public String toString() { buffer.append(range.toString()); } + if (exclusion != null) { + buffer.append(" EXCLUDE ").append(exclusion); + } + return buffer.toString(); } @@ -69,11 +83,31 @@ public WindowElement withRange(WindowRange range) { return this; } + public WindowElement withExclusion(Exclusion exclusion) { + this.setExclusion(exclusion); + return this; + } + public enum Type { - ROWS, RANGE; + ROWS, RANGE, GROUPS; public static Type from(String type) { - return Enum.valueOf(Type.class, type.toUpperCase()); + return Enum.valueOf(Type.class, type.toUpperCase(Locale.ROOT)); + } + } + + public enum Exclusion { + CURRENT_ROW("CURRENT ROW"), GROUP("GROUP"), TIES("TIES"), NO_OTHERS("NO OTHERS"); + + private final String keyword; + + Exclusion(String keyword) { + this.keyword = keyword; + } + + @Override + public String toString() { + return keyword; } } diff --git a/src/main/java/net/sf/jsqlparser/expression/WindowOffset.java b/src/main/java/net/sf/jsqlparser/expression/WindowOffset.java index 0303b4f06..079ec2eba 100644 --- a/src/main/java/net/sf/jsqlparser/expression/WindowOffset.java +++ b/src/main/java/net/sf/jsqlparser/expression/WindowOffset.java @@ -10,6 +10,7 @@ package net.sf.jsqlparser.expression; import java.io.Serializable; +import java.util.Locale; public class WindowOffset implements Serializable { @@ -79,7 +80,7 @@ public enum Type { PRECEDING, FOLLOWING, CURRENT, EXPR; public static Type from(String type) { - return Enum.valueOf(Type.class, type.toUpperCase()); + return Enum.valueOf(Type.class, type.toUpperCase(Locale.ROOT)); } } diff --git a/src/main/java/net/sf/jsqlparser/expression/XmlTableFunction.java b/src/main/java/net/sf/jsqlparser/expression/XmlTableFunction.java new file mode 100644 index 000000000..9a4f4210b --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/expression/XmlTableFunction.java @@ -0,0 +1,231 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.expression; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.statement.create.table.ColDataType; + +/** + * Represents an {@code XMLTABLE} table function (SQL/XML standard, supported by PostgreSQL and + * Oracle), which turns an XML document into a relational row set, e.g.: + * + *
+ * XMLTABLE('//ROWS/ROW'
+ *          PASSING data
+ *          COLUMNS id int PATH '@id',
+ *                  ordinality FOR ORDINALITY,
+ *                  country_id text PATH 'COUNTRY_ID',
+ *                  size float PATH 'SIZE' DEFAULT 0)
+ * 
+ * + * It is the XML counterpart of {@link JsonTableFunction} and mirrors its structure, without the + * JSON-specific wrapper/error/plan clauses. + */ +public class XmlTableFunction extends Function { + + private Expression rowPathExpression; + private final List passingClauses = new ArrayList<>(); + private final List columnDefinitions = new ArrayList<>(); + + public static class XmlTablePassingClause extends ASTNodeAccessImpl implements Serializable { + private Expression valueExpression; + private String name; + + public Expression getValueExpression() { + return valueExpression; + } + + public XmlTablePassingClause setValueExpression(Expression valueExpression) { + this.valueExpression = valueExpression; + return this; + } + + public String getName() { + return name; + } + + public XmlTablePassingClause setName(String name) { + this.name = name; + return this; + } + + public void collectExpressions(List expressions) { + if (valueExpression != null) { + expressions.add(valueExpression); + } + } + + @Override + public String toString() { + return valueExpression + (name != null ? " AS " + name : ""); + } + } + + public static class XmlTableColumnDefinition extends ASTNodeAccessImpl implements Serializable { + private String columnName; + private boolean forOrdinality; + private ColDataType dataType; + private Expression pathExpression; + private Expression defaultExpression; + + public String getColumnName() { + return columnName; + } + + public XmlTableColumnDefinition setColumnName(String columnName) { + this.columnName = columnName; + return this; + } + + public boolean isForOrdinality() { + return forOrdinality; + } + + public XmlTableColumnDefinition setForOrdinality(boolean forOrdinality) { + this.forOrdinality = forOrdinality; + return this; + } + + public ColDataType getDataType() { + return dataType; + } + + public XmlTableColumnDefinition setDataType(ColDataType dataType) { + this.dataType = dataType; + return this; + } + + public Expression getPathExpression() { + return pathExpression; + } + + public XmlTableColumnDefinition setPathExpression(Expression pathExpression) { + this.pathExpression = pathExpression; + return this; + } + + public Expression getDefaultExpression() { + return defaultExpression; + } + + public XmlTableColumnDefinition setDefaultExpression(Expression defaultExpression) { + this.defaultExpression = defaultExpression; + return this; + } + + public void collectExpressions(List expressions) { + if (pathExpression != null) { + expressions.add(pathExpression); + } + if (defaultExpression != null) { + expressions.add(defaultExpression); + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(columnName); + if (forOrdinality) { + builder.append(" FOR ORDINALITY"); + return builder.toString(); + } + if (dataType != null) { + builder.append(" ").append(dataType); + } + if (pathExpression != null) { + builder.append(" PATH ").append(pathExpression); + } + if (defaultExpression != null) { + builder.append(" DEFAULT ").append(defaultExpression); + } + return builder.toString(); + } + } + + public Expression getRowPathExpression() { + return rowPathExpression; + } + + public XmlTableFunction setRowPathExpression(Expression rowPathExpression) { + this.rowPathExpression = rowPathExpression; + return this; + } + + public List getPassingClauses() { + return passingClauses; + } + + public XmlTableFunction addPassingClause(XmlTablePassingClause passingClause) { + passingClauses.add(passingClause); + return this; + } + + public List getColumnDefinitions() { + return columnDefinitions; + } + + public XmlTableFunction addColumnDefinition(XmlTableColumnDefinition columnDefinition) { + columnDefinitions.add(columnDefinition); + return this; + } + + public List getAllExpressions() { + List expressions = new ArrayList<>(); + if (rowPathExpression != null) { + expressions.add(rowPathExpression); + } + for (XmlTablePassingClause passingClause : passingClauses) { + passingClause.collectExpressions(expressions); + } + for (XmlTableColumnDefinition columnDefinition : columnDefinitions) { + columnDefinition.collectExpressions(expressions); + } + return expressions; + } + + @Override + public T accept(ExpressionVisitor expressionVisitor, S context) { + return expressionVisitor.visit(this, context); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("XMLTABLE("); + builder.append(rowPathExpression); + if (!passingClauses.isEmpty()) { + builder.append(" PASSING "); + boolean first = true; + for (XmlTablePassingClause passingClause : passingClauses) { + if (!first) { + builder.append(", "); + } + builder.append(passingClause); + first = false; + } + } + if (!columnDefinitions.isEmpty()) { + builder.append(" COLUMNS "); + boolean first = true; + for (XmlTableColumnDefinition columnDefinition : columnDefinitions) { + if (!first) { + builder.append(", "); + } + builder.append(columnDefinition); + first = false; + } + } + builder.append(")"); + return builder.toString(); + } +} diff --git a/src/main/java/net/sf/jsqlparser/expression/operators/relational/FullTextSearch.java b/src/main/java/net/sf/jsqlparser/expression/operators/relational/FullTextSearch.java index f191ae1a1..0bf79f0ec 100644 --- a/src/main/java/net/sf/jsqlparser/expression/operators/relational/FullTextSearch.java +++ b/src/main/java/net/sf/jsqlparser/expression/operators/relational/FullTextSearch.java @@ -9,6 +9,10 @@ */ package net.sf.jsqlparser.expression.operators.relational; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.Optional; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.ExpressionVisitor; import net.sf.jsqlparser.expression.JdbcNamedParameter; @@ -17,11 +21,6 @@ import net.sf.jsqlparser.parser.ASTNodeAccessImpl; import net.sf.jsqlparser.schema.Column; -import java.util.Arrays; -import java.util.Collection; -import java.util.Iterator; -import java.util.Optional; - public class FullTextSearch extends ASTNodeAccessImpl implements Expression { private ExpressionList _matchColumns; @@ -44,16 +43,20 @@ public Expression getAgainstValue() { return this._againstValue; } - public void setAgainstValue(StringValue val) { + public void setAgainstValue(Expression val) { this._againstValue = val; } + public void setAgainstValue(StringValue val) { + setAgainstValue((Expression) val); + } + public void setAgainstValue(JdbcNamedParameter val) { - this._againstValue = val; + setAgainstValue((Expression) val); } public void setAgainstValue(JdbcParameter val) { - this._againstValue = val; + setAgainstValue((Expression) val); } public String getSearchModifier() { @@ -92,6 +95,10 @@ public FullTextSearch withMatchColumns(ExpressionList matchColumns) { } public FullTextSearch withAgainstValue(StringValue againstValue) { + return withAgainstValue((Expression) againstValue); + } + + public FullTextSearch withAgainstValue(Expression againstValue) { this.setAgainstValue(againstValue); return this; } diff --git a/src/main/java/net/sf/jsqlparser/expression/operators/relational/LikeExpression.java b/src/main/java/net/sf/jsqlparser/expression/operators/relational/LikeExpression.java index f450de1c1..770db3de3 100644 --- a/src/main/java/net/sf/jsqlparser/expression/operators/relational/LikeExpression.java +++ b/src/main/java/net/sf/jsqlparser/expression/operators/relational/LikeExpression.java @@ -9,6 +9,7 @@ */ package net.sf.jsqlparser.expression.operators.relational; +import java.util.Locale; import net.sf.jsqlparser.expression.BinaryExpression; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.ExpressionVisitor; @@ -120,7 +121,8 @@ public enum KeyWord { LIKE, ILIKE, RLIKE, REGEXP_LIKE, REGEXP, SIMILAR_TO, MATCH_ANY, MATCH_ALL, MATCH_PHRASE, MATCH_PHRASE_PREFIX, MATCH_REGEXP; public static KeyWord from(String keyword) { - return Enum.valueOf(KeyWord.class, keyword.toUpperCase().replaceAll("\\s+", "_")); + return Enum.valueOf(KeyWord.class, + keyword.toUpperCase(Locale.ROOT).replaceAll("\\s+", "_")); } } } 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/ParserKeywordsUtils.java b/src/main/java/net/sf/jsqlparser/parser/ParserKeywordsUtils.java index bfaa4a647..5bdb93830 100644 --- a/src/main/java/net/sf/jsqlparser/parser/ParserKeywordsUtils.java +++ b/src/main/java/net/sf/jsqlparser/parser/ParserKeywordsUtils.java @@ -10,425 +10,215 @@ package net.sf.jsqlparser.parser; import java.io.File; -import java.io.FileNotFoundException; import java.io.FileWriter; import java.io.IOException; -import java.nio.charset.Charset; import java.nio.charset.CharsetEncoder; import java.nio.charset.StandardCharsets; import java.nio.file.Files; -import java.nio.file.Path; -import java.util.*; +import java.util.TreeSet; import java.util.regex.Matcher; import java.util.regex.Pattern; +/** + * Utilities for querying the parser's reserved and non-reserved keyword sets. + * + *

+ * Non-reserved keywords are derived from the generated {@link CCJSqlParserConstants} token + * table using the {@code MIN_NON_RESERVED_WORD} / {@code MAX_NON_RESERVED_WORD} sentinels. + * + *

+ * Reserved keywords are determined by scanning the Grammar file for all simple string token + * definitions ({@code }) and subtracting the non-reserved set. + */ public class ParserKeywordsUtils { - public final static CharsetEncoder CHARSET_ENCODER = StandardCharsets.US_ASCII.newEncoder(); - - public final static int RESTRICTED_FUNCTION = 1; - public final static int RESTRICTED_SCHEMA = 2; - public final static int RESTRICTED_TABLE = 4; - public final static int RESTRICTED_COLUMN = 8; - public final static int RESTRICTED_EXPRESSION = 16; - public final static int RESTRICTED_ALIAS = 32; - public final static int RESTRICTED_SQL2016 = 64; - - public final static int RESTRICTED_JSQLPARSER = 128 - | RESTRICTED_FUNCTION - | RESTRICTED_SCHEMA - | RESTRICTED_TABLE - | RESTRICTED_COLUMN - | RESTRICTED_EXPRESSION - | RESTRICTED_ALIAS - | RESTRICTED_SQL2016; + private static final CharsetEncoder ASCII_ENCODER = StandardCharsets.US_ASCII.newEncoder(); - // Classification follows http://www.h2database.com/html/advanced.html#keywords - public final static Object[][] ALL_RESERVED_KEYWORDS = { - {"ABSENT", RESTRICTED_JSQLPARSER}, - {"ALL", RESTRICTED_SQL2016}, - {"AND", RESTRICTED_SQL2016}, - {"ANY", RESTRICTED_JSQLPARSER}, - {"AS", RESTRICTED_SQL2016}, - {"BETWEEN", RESTRICTED_SQL2016}, - {"BOTH", RESTRICTED_SQL2016}, - {"CASEWHEN", RESTRICTED_ALIAS}, - {"CHECK", RESTRICTED_SQL2016}, - {"CONNECT", RESTRICTED_ALIAS}, - {"CONNECT_BY_ROOT", RESTRICTED_JSQLPARSER}, - {"CSV", RESTRICTED_JSQLPARSER}, - {"PRIOR", RESTRICTED_JSQLPARSER}, - {"CONSTRAINT", RESTRICTED_SQL2016}, - {"CREATE", RESTRICTED_ALIAS}, - {"CROSS", RESTRICTED_SQL2016}, - {"CURRENT", RESTRICTED_JSQLPARSER}, - {"DEFAULT", RESTRICTED_ALIAS}, - {"DISTINCT", RESTRICTED_SQL2016}, - {"DISTINCTROW", RESTRICTED_SQL2016}, - {"DOUBLE", RESTRICTED_ALIAS}, - {"ELSE", RESTRICTED_JSQLPARSER}, - {"ERRORS", RESTRICTED_JSQLPARSER}, - {"EXCEPT", RESTRICTED_SQL2016}, - {"EXCLUDES", RESTRICTED_JSQLPARSER}, - {"EXISTS", RESTRICTED_SQL2016}, - {"EXTEND", RESTRICTED_JSQLPARSER}, - {"FALSE", RESTRICTED_SQL2016}, - {"FBV", RESTRICTED_JSQLPARSER}, - {"FETCH", RESTRICTED_SQL2016}, - {"FILE", RESTRICTED_JSQLPARSER}, - {"FINAL", RESTRICTED_JSQLPARSER}, - {"FOR", RESTRICTED_SQL2016}, - {"FORCE", RESTRICTED_SQL2016}, - {"FOREIGN", RESTRICTED_SQL2016}, - {"FROM", RESTRICTED_SQL2016}, - {"FULL", RESTRICTED_SQL2016}, - {"GLOBAL", RESTRICTED_ALIAS}, - {"GROUP", RESTRICTED_SQL2016}, - {"GROUPING", RESTRICTED_ALIAS}, - {"QUALIFY", RESTRICTED_ALIAS}, - {"HAVING", RESTRICTED_SQL2016}, - {"IF", RESTRICTED_SQL2016}, - {"IIF", RESTRICTED_ALIAS}, - {"IGNORE", RESTRICTED_ALIAS}, - {"ILIKE", RESTRICTED_SQL2016}, - {"IMPORT", RESTRICTED_JSQLPARSER}, - {"IN", RESTRICTED_SQL2016}, - {"INCLUDES", RESTRICTED_JSQLPARSER}, - {"INNER", RESTRICTED_SQL2016}, - {"INTERSECT", RESTRICTED_SQL2016}, - {"INTERVAL", RESTRICTED_SQL2016}, - {"INTO", RESTRICTED_JSQLPARSER}, - {"IS", RESTRICTED_SQL2016}, - {"JOIN", RESTRICTED_JSQLPARSER}, - {"LATERAL", RESTRICTED_SQL2016}, - {"LEFT", RESTRICTED_SQL2016}, - {"LIKE", RESTRICTED_SQL2016}, - {"LIMIT", RESTRICTED_SQL2016}, - {"MINUS", RESTRICTED_SQL2016}, - {"NATURAL", RESTRICTED_SQL2016}, - {"NOCYCLE", RESTRICTED_JSQLPARSER}, - {"NOT", RESTRICTED_SQL2016}, - {"NULL", RESTRICTED_SQL2016}, - {"OFFSET", RESTRICTED_SQL2016}, - {"ON", RESTRICTED_SQL2016}, - {"ONLY", RESTRICTED_JSQLPARSER}, - {"OPTIMIZE", RESTRICTED_ALIAS}, - {"OR", RESTRICTED_SQL2016}, - {"ORDER", RESTRICTED_SQL2016}, - {"OUTER", RESTRICTED_JSQLPARSER}, - {"OUTPUT", RESTRICTED_JSQLPARSER}, - {"OPTIMIZE ", RESTRICTED_JSQLPARSER}, - {"OVERWRITE ", RESTRICTED_JSQLPARSER}, - {"PIVOT", RESTRICTED_JSQLPARSER}, - {"PREFERRING", RESTRICTED_JSQLPARSER}, - {"PRIOR", RESTRICTED_ALIAS}, - {"PROCEDURE", RESTRICTED_ALIAS}, - {"PUBLIC", RESTRICTED_ALIAS}, - {"RETURNING", RESTRICTED_JSQLPARSER}, - {"RIGHT", RESTRICTED_SQL2016}, - {"SAMPLE", RESTRICTED_ALIAS}, - {"SCRIPT", RESTRICTED_JSQLPARSER}, - {"SEL", RESTRICTED_ALIAS}, - {"SELECT", RESTRICTED_ALIAS}, - {"SEMI", RESTRICTED_JSQLPARSER}, - {"SET", RESTRICTED_JSQLPARSER}, - {"SOME", RESTRICTED_JSQLPARSER}, - {"START", RESTRICTED_JSQLPARSER}, - {"STATEMENT", RESTRICTED_JSQLPARSER}, - {"TABLES", RESTRICTED_ALIAS}, - {"TOP", RESTRICTED_SQL2016}, - {"TRAILING", RESTRICTED_SQL2016}, - {"TRUE", RESTRICTED_SQL2016}, - {"UNBOUNDED", RESTRICTED_JSQLPARSER}, - {"UNION", RESTRICTED_SQL2016}, - {"UNIQUE", RESTRICTED_SQL2016}, - {"UNKNOWN", RESTRICTED_SQL2016}, - {"UNPIVOT", RESTRICTED_JSQLPARSER}, - {"USE", RESTRICTED_JSQLPARSER}, - {"USING", RESTRICTED_SQL2016}, - {"SQL_CACHE", RESTRICTED_JSQLPARSER}, - {"SQL_CALC_FOUND_ROWS", RESTRICTED_JSQLPARSER}, - {"SQL_NO_CACHE", RESTRICTED_JSQLPARSER}, - {"STRAIGHT_JOIN", RESTRICTED_JSQLPARSER}, - {"TABLESAMPLE", RESTRICTED_ALIAS}, - {"VALUE", RESTRICTED_JSQLPARSER}, - {"VALUES", RESTRICTED_SQL2016}, - {"VARYING", RESTRICTED_JSQLPARSER}, - {"VERIFY", RESTRICTED_JSQLPARSER}, - {"WHEN", RESTRICTED_SQL2016}, - {"WHERE", RESTRICTED_SQL2016}, - {"WINDOW", RESTRICTED_SQL2016}, - {"WITH", RESTRICTED_SQL2016}, - {"XOR", RESTRICTED_JSQLPARSER}, - {"XMLSERIALIZE", RESTRICTED_JSQLPARSER}, + /** Matches a pure keyword image: word characters, at least two characters, pure US-ASCII. */ + private static final Pattern KEYWORD_PATTERN = Pattern.compile("[A-Za-z_][A-Za-z_0-9]+"); - // add keywords from the composite token definitions: - // tk= | tk= | tk= - // we will use the composite tokens instead, which are always hit first before the - // simple keywords - // @todo: figure out a way to remove these composite tokens, as they do more harm than - // good - {"SEL", RESTRICTED_JSQLPARSER}, - {"SELECT", RESTRICTED_JSQLPARSER}, - {"DATE", RESTRICTED_JSQLPARSER}, - {"TIME", RESTRICTED_JSQLPARSER}, - {"TIMESTAMP", RESTRICTED_JSQLPARSER}, - {"YEAR", RESTRICTED_JSQLPARSER}, - {"MONTH", RESTRICTED_JSQLPARSER}, - {"DAY", RESTRICTED_JSQLPARSER}, - {"HOUR", RESTRICTED_JSQLPARSER}, - {"MINUTE", RESTRICTED_JSQLPARSER}, - {"SECOND", RESTRICTED_JSQLPARSER}, - {"SUBSTR", RESTRICTED_JSQLPARSER}, - {"SUBSTRING", RESTRICTED_JSQLPARSER}, - {"TRIM", RESTRICTED_JSQLPARSER}, - {"POSITION", RESTRICTED_JSQLPARSER}, - {"OVERLAY", RESTRICTED_JSQLPARSER}, - {"NEXTVAL", RESTRICTED_COLUMN}, - - // @todo: Object Names should not start with Hex-Prefix, we shall not find that Token - {"0x", RESTRICTED_JSQLPARSER} - }; + /** + * Matches simple token definitions in the grammar: {@code }. Group 1 captures + * the string value. Only matches definitions where the value is a plain quoted string — + * compound regex tokens like {@code } won't match. + */ + private static final Pattern SIMPLE_TOKEN_PATTERN = + Pattern.compile("", Pattern.MULTILINE); - @SuppressWarnings({"PMD.ExcessiveMethodLength"}) - public static List getReservedKeywords(int restriction) { - ArrayList keywords = new ArrayList<>(); - for (Object[] data : ALL_RESERVED_KEYWORDS) { - int value = (int) data[1]; + private ParserKeywordsUtils() { + // utility class + } - // test if bit is not set - if ((value & restriction) == restriction || (restriction & value) == value) { - keywords.add((String) data[0]); + /** + * Returns the set of non-reserved keywords, i.e. tokens whose kind lies between + * {@code MIN_NON_RESERVED_WORD} and {@code MAX_NON_RESERVED_WORD}. These keywords can be used + * as unquoted identifiers. + */ + public static TreeSet getNonReservedKeywords() { + TreeSet keywords = new TreeSet<>(); + String[] images = CCJSqlParserConstants.tokenImage; + + for (int kind = CCJSqlParserConstants.MIN_NON_RESERVED_WORD + + 1; kind < CCJSqlParserConstants.MAX_NON_RESERVED_WORD; kind++) { + String image = extractKeyword(images[kind]); + if (image != null && isKeywordImage(image)) { + keywords.add(image); } } - return keywords; } /** - * @param args with: Grammar File, Keyword Documentation File - * @throws Exception + * Returns the set of reserved keywords by scanning the Grammar file for all simple + * string token definitions and subtracting the non-reserved keywords. + * + * @param grammarFile the {@code .jjt} grammar file + * @return reserved keyword strings + * @throws IOException if reading the grammar file fails */ - public static void main(String[] args) throws Exception { - if (args.length < 2) { - throw new IllegalArgumentException("No filename provided aS context ARGS[0]"); - } - - File grammarFile = new File(args[0]); - if (grammarFile.exists() && grammarFile.canRead() && grammarFile.canWrite()) { - buildGrammarForRelObjectName(grammarFile); - buildGrammarForRelObjectNameWithoutValue(grammarFile); - } else { - throw new FileNotFoundException("Can't read file " + args[0]); - } - - File keywordDocumentationFile = new File(args[1]); - keywordDocumentationFile.createNewFile(); - if (keywordDocumentationFile.canWrite()) { - writeKeywordsDocumentationFile(keywordDocumentationFile); - } else { - throw new FileNotFoundException("Can't read file " + args[1]); - } + public static TreeSet getReservedKeywords(File grammarFile) throws IOException { + TreeSet allSimple = getAllSimpleKeywords(grammarFile); + allSimple.removeAll(getNonReservedKeywords()); + return allSimple; } - public static TreeSet getAllKeywordsUsingRegex(File file) throws IOException { - Pattern tokenBlockPattern = Pattern.compile( - "TOKEN\\s*:\\s*/\\*.*\\*/*(?:\\r?\\n|\\r)\\{(?:[^}{]+|\\{(?:[^}{]+|\\{[^}{]*})*})*}", - Pattern.MULTILINE); - Pattern tokenStringValuePattern = Pattern.compile("\"(\\w{2,})\"", Pattern.MULTILINE); - - TreeSet allKeywords = new TreeSet<>(); - - Path path = file.toPath(); - Charset charset = Charset.defaultCharset(); - String content = new String(Files.readAllBytes(path), charset); - - Matcher tokenBlockmatcher = tokenBlockPattern.matcher(content); - while (tokenBlockmatcher.find()) { - String tokenBlock = tokenBlockmatcher.group(0); - // remove single and multiline comments - tokenBlock = tokenBlock.replaceAll("(?sm)((\\/\\*.*?\\*\\/)|(\\/\\/.*?$))", ""); - for (String tokenDefinition : getTokenDefinitions(tokenBlock)) { - // check if token definition is private - if (tokenDefinition.matches("(?sm)^<\\s*[^#].*")) { - Matcher tokenStringValueMatcher = - tokenStringValuePattern.matcher(tokenDefinition); - while (tokenStringValueMatcher.find()) { - String tokenValue = tokenStringValueMatcher.group(1); - // test if pure US-ASCII - if (CHARSET_ENCODER.canEncode(tokenValue) && tokenValue.matches("\\w+")) { - allKeywords.add(tokenValue); - } - } - } + /** + * Returns all simple string keywords defined in the grammar file. Scans for + * {@code } patterns and collects the string values. + * + * @param grammarFile the {@code .jjt} grammar file + * @return all simple keyword strings + * @throws IOException if reading the grammar file fails + */ + public static TreeSet getAllSimpleKeywords(File grammarFile) throws IOException { + TreeSet keywords = new TreeSet<>(); + String content = Files.readString(grammarFile.toPath(), StandardCharsets.UTF_8); + + Matcher matcher = SIMPLE_TOKEN_PATTERN.matcher(content); + while (matcher.find()) { + String value = matcher.group(1); + if (isKeywordImage(value) && ASCII_ENCODER.canEncode(value)) { + keywords.add(value); } } - return allKeywords; + return keywords; } - @SuppressWarnings({"PMD.EmptyWhileStmt"}) - private static List getTokenDefinitions(String tokenBlock) { - List tokenDefinitions = new ArrayList<>(); - int level = 0; - char openChar = '<'; - char closeChar = '>'; - char[] tokenBlockChars = tokenBlock.toCharArray(); - int tokenDefinitionStart = -1; - for (int i = 0; i < tokenBlockChars.length; ++i) { - if (isQuotationMark(i, tokenBlockChars)) { - // skip everything inside quotation marks - while (!isQuotationMark(++i, tokenBlockChars)) { - // skip until quotation ends - } - } - - char character = tokenBlockChars[i]; - if (character == openChar) { - if (level == 0) { - tokenDefinitionStart = i; - } + /** + * Checks whether the given token kind is a non-reserved keyword that can be used as an unquoted + * identifier. + */ + public static boolean isNonReservedKeyword(int tokenKind) { + return tokenKind > CCJSqlParserConstants.MIN_NON_RESERVED_WORD + && tokenKind < CCJSqlParserConstants.MAX_NON_RESERVED_WORD; + } - ++level; - } else if (character == closeChar) { - --level; + /** + * Writes a reStructuredText documentation file listing all reserved keywords. + * + * @param grammarFile the {@code .jjt} grammar file + * @param rstFile the output {@code .rst} file to write + * @throws IOException if reading or writing fails + */ + public static void writeKeywordsDocumentationFile(File grammarFile, File rstFile) + throws IOException { + TreeSet reserved = getReservedKeywords(grammarFile); - if (level == 0 && tokenDefinitionStart >= 0) { - tokenDefinitions.add(tokenBlock.substring(tokenDefinitionStart, i + 1)); - tokenDefinitionStart = -1; - } - } - } + StringBuilder builder = new StringBuilder(); + builder.append("***********************\n"); + builder.append("Reserved Keywords\n"); + builder.append("***********************\n"); + builder.append("\n"); - return tokenDefinitions; - } + builder.append( + "The following Keywords are **reserved** in JSQLParser-|JSQLPARSER_VERSION| and must not be used for **Naming Objects**: \n"); + builder.append("\n"); - private static boolean isQuotationMark(int index, char[] str) { - if (str[index] == '\"') { - // check if quotation is escaped - if (index > 0 && str[index - 1] == '\\') { - return index > 1 && str[index - 2] == '\\'; - } + builder.append("+---------------------------+\n"); + builder.append("| **Keyword** |\n"); + builder.append("+---------------------------+\n"); - return true; + for (String keyword : reserved) { + builder.append("| ").append(rightPadding(keyword, ' ', 25)).append(" |\n"); + builder.append("+---------------------------+\n"); } - return false; + try (FileWriter fileWriter = new FileWriter(rstFile)) { + fileWriter.append(builder); + fileWriter.flush(); + } } - public static void buildGrammarForRelObjectNameWithoutValue(File file) throws Exception { - Pattern methodBlockPattern = Pattern.compile( - "String\\W*RelObjectNameWithoutValue\\W*\\(\\W*\\)\\W*:\\s*\\{(?:[^}{]+|\\{(?:[^}{]+|\\{[^}{]*})*})*}\\s*\\{(?:[^}{]+|\\{(?:[^}{]+|\\{[^}{]*})*})*}", - Pattern.MULTILINE); - - TreeSet allKeywords = getAllKeywords(file); + public static String rightPadding(String input, char ch, int length) { + return String.format("%" + (-length) + "s", input).replace(' ', ch); + } - for (String reserved : getReservedKeywords(RESTRICTED_JSQLPARSER)) { - allKeywords.remove(reserved); + /** + * Entry point for the {@code updateKeywords} Gradle/Maven task. + * + *

+ * Usage: {@code java net.sf.jsqlparser.parser.ParserKeywordsUtils } + * + * @param args {@code args[0]}: path to the grammar file, {@code args[1]}: path to the output + * RST file + * @throws Exception if reading or writing fails + */ + public static void main(String[] args) throws Exception { + if (args.length < 2) { + throw new IllegalArgumentException( + "Usage: ParserKeywordsUtils "); } - StringBuilder builder = new StringBuilder(); - builder.append("String RelObjectNameWithoutValue() :\n" - + "{ Token tk = null; }\n" - + "{\n" - // @todo: find a way to avoid those hardcoded compound tokens - + " ( tk= | tk= | tk= | tk= | tk= | tk= | tk= | tk= | tk= \n" - + " "); - - for (String keyword : allKeywords) { - builder.append(" | tk=\"").append(keyword).append("\""); + File grammarFile = new File(args[0]); + if (!grammarFile.canRead()) { + throw new IOException("Cannot read grammar file: " + grammarFile); } - builder.append(" )\n" + " { return tk.image; }\n" + "}"); + File rstFile = new File(args[1]); + rstFile.getParentFile().mkdirs(); + writeKeywordsDocumentationFile(grammarFile, rstFile); - replaceInFile(file, methodBlockPattern, builder.toString()); + System.out.println("Reserved keywords: " + getReservedKeywords(grammarFile).size()); + System.out.println("Non-reserved keywords: " + getNonReservedKeywords().size()); + System.out.println("Written to: " + rstFile.getAbsolutePath()); } - public static void buildGrammarForRelObjectName(File file) throws Exception { - // Pattern pattern = - // Pattern.compile("String\\W*RelObjectName\\W*\\(\\W*\\)\\W*:\\s*\\{(?:[^}{]+|\\{(?:[^}{]+|\\{[^}{]*})*})*}\\s*\\{(?:[^}{]+|\\{(?:[^}{]+|\\{[^}{]*})*})*}", - // Pattern.MULTILINE); - TreeSet allKeywords = new TreeSet<>(); - for (String reserved : getReservedKeywords(RESTRICTED_ALIAS)) { - allKeywords.add(reserved); + /** + * Extracts the keyword string from a {@code tokenImage} entry. + * + *

+ * JavaCC renders inline BNF token declarations as {@code } in {@code tokenImage}. + * Stripping the {@code K_} prefix and angle brackets yields the keyword string. + * + * @return the keyword string, or {@code null} if the entry is not a {@code K_} token + */ + private static String extractKeyword(String tokenImage) { + if (tokenImage == null || tokenImage.length() < 5) { + return null; } - for (String reserved : getReservedKeywords(RESTRICTED_JSQLPARSER & ~RESTRICTED_ALIAS)) { - allKeywords.remove(reserved); + // Format: → ACTION + if (tokenImage.charAt(0) == '<' + && tokenImage.charAt(tokenImage.length() - 1) == '>' + && tokenImage.startsWith(" getAllKeywords(File file) throws Exception { - return getAllKeywordsUsingRegex(file); - } - - private static void replaceInFile(File file, Pattern pattern, String replacement) - throws IOException { - Path path = file.toPath(); - Charset charset = Charset.defaultCharset(); - - String content = new String(Files.readAllBytes(path), charset); - content = pattern.matcher(content).replaceAll(replacement); - Files.write(file.toPath(), content.getBytes(charset)); + return null; } - public static String rightPadding(String input, char ch, int length) { - return String.format("%" + (-length) + "s", input).replace(' ', ch); - } - - public static void writeKeywordsDocumentationFile(File file) throws IOException { - StringBuilder builder = new StringBuilder(); - builder.append("***********************\n"); - builder.append("Restricted Keywords\n"); - builder.append("***********************\n"); - builder.append("\n"); - - builder.append( - "The following Keywords are **restricted** in JSQLParser-|JSQLPARSER_VERSION| and must not be used for **Naming Objects**: \n"); - builder.append("\n"); - - builder.append("+----------------------+-------------+-----------+\n"); - builder.append("| **Keyword** | JSQL Parser | SQL:2016 |\n"); - builder.append("+----------------------+-------------+-----------+\n"); - - for (Object[] keywordDefinition : ALL_RESERVED_KEYWORDS) { - builder.append("| ").append(rightPadding(keywordDefinition[0].toString(), ' ', 20)) - .append(" | "); - - int value = (int) keywordDefinition[1]; - int restriction = RESTRICTED_JSQLPARSER; - String s = (value & restriction) == restriction || (restriction & value) == value - ? "Yes" - : ""; - builder.append(rightPadding(s, ' ', 11)).append(" | "); - - restriction = RESTRICTED_SQL2016; - s = (value & restriction) == restriction || (restriction & value) == value - ? "Yes" - : ""; - builder.append(rightPadding(s, ' ', 9)).append(" | "); - - builder.append("\n"); - builder.append("+----------------------+-------------+-----------+\n"); - } - try (FileWriter fileWriter = new FileWriter(file)) { - fileWriter.append(builder); - fileWriter.flush(); - } + /** + * Returns {@code true} if the image looks like a SQL keyword: alphabetic start, word characters + * only, at least 2 characters, pure US-ASCII. + */ + private static boolean isKeywordImage(String image) { + return KEYWORD_PATTERN.matcher(image).matches() + && ASCII_ENCODER.canEncode(image); } } 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/java/net/sf/jsqlparser/parser/feature/Feature.java b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java index 7f4cf2af0..4c1b6dd51 100644 --- a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java +++ b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java @@ -30,6 +30,7 @@ import net.sf.jsqlparser.statement.alter.sequence.AlterSequence; import net.sf.jsqlparser.statement.analyze.Analyze; import net.sf.jsqlparser.statement.comment.Comment; +import net.sf.jsqlparser.statement.create.database.CreateDatabase; import net.sf.jsqlparser.statement.create.function.CreateFunction; import net.sf.jsqlparser.statement.create.index.CreateIndex; import net.sf.jsqlparser.statement.create.procedure.CreateProcedure; @@ -475,6 +476,12 @@ public enum Feature { */ drop, dropTable, dropIndex, dropView, dropSchema, dropSequence, dropTableIfExists, dropIndexIfExists, dropViewIfExists, dropSchemaIfExists, dropSequenceIfExists, + /** + * SQL "CREATE DATABASE" statement is allowed + * + * @see CreateDatabase + */ + createDatabase, /** * SQL "CREATE SCHEMA" statement is allowed * @@ -809,6 +816,19 @@ public enum Feature { * "EXPORT" */ export, + + /** + * MySQL allows a ',' as a separator between key and value entries. We allow that by default, + * but it can be disabled here + */ + allowCommaAsKeyValueSeparator(true), + + /** + * DB2 and Oracle allow Expressions as JSON_OBJECT key values. This clashes with Informix and + * Snowflake Json-Extraction syntax + */ + allowExpressionAsJsonObjectKey(false) + ; private final Object value; diff --git a/src/main/java/net/sf/jsqlparser/schema/Column.java b/src/main/java/net/sf/jsqlparser/schema/Column.java index 400d34c3a..33240ac52 100644 --- a/src/main/java/net/sf/jsqlparser/schema/Column.java +++ b/src/main/java/net/sf/jsqlparser/schema/Column.java @@ -12,11 +12,12 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; - import net.sf.jsqlparser.expression.ArrayConstructor; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.ExpressionVisitor; +import net.sf.jsqlparser.expression.operators.relational.SupportsOldOracleJoinSyntax; import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.statement.ReturningReferenceType; /** * A column. It can have the table name it belongs to. @@ -28,6 +29,9 @@ public class Column extends ASTNodeAccessImpl implements Expression, MultiPartNa private String commentText; private ArrayConstructor arrayConstructor; private String tableDelimiter = "."; + private int oldOracleJoinSyntax = SupportsOldOracleJoinSyntax.NO_ORACLE_JOIN; + private ReturningReferenceType returningReferenceType = null; + private String returningQualifier = null; // holds the physical table when resolved against an actual schema information private Table resolvedTable = null; @@ -53,7 +57,8 @@ public Column(List nameParts, List delimiters) { } public Column(String columnName) { - this(null, columnName); + this(); + setColumnName(columnName); } public ArrayConstructor getArrayConstructor() { @@ -131,8 +136,56 @@ public String getUnquotedColumnName() { return MultiPartName.unquote(columnName); } - public void setColumnName(String string) { - columnName = string; + public void setColumnName(String name) { + // BigQuery seems to allow things like: `catalogName.schemaName.tableName` in only one pair + // of quotes + // however, some people believe that Dots in Names are a good idea, so provide a switch-off + boolean splitNamesOnDelimiter = System.getProperty("SPLIT_NAMES_ON_DELIMITER") == null || + !List + .of("0", "N", "n", "FALSE", "false", "OFF", "off") + .contains(System.getProperty("SPLIT_NAMES_ON_DELIMITER")); + + setName(name, splitNamesOnDelimiter); + } + + public void setName(String name, boolean splitNamesOnDelimiter) { + if (MultiPartName.isQuoted(name) && name.contains(".") && splitNamesOnDelimiter) { + String[] parts = MultiPartName.unquote(name).split("\\."); + switch (parts.length) { + case 3: + this.table = new Table("\"" + parts[0] + "\".\"" + parts[1] + "\""); + this.columnName = "\"" + parts[2] + "\""; + break; + case 2: + this.table = new Table("\"" + parts[0] + "\""); + this.columnName = "\"" + parts[1] + "\""; + break; + case 1: + this.columnName = "\"" + parts[0] + "\""; + break; + default: + throw new RuntimeException("Invalid column name: " + name); + } + } else if (name.contains(".") && splitNamesOnDelimiter) { + String[] parts = MultiPartName.unquote(name).split("\\."); + switch (parts.length) { + case 3: + this.table = new Table(parts[0] + "." + parts[1]); + this.columnName = parts[2]; + break; + case 2: + this.table = new Table(parts[0]); + this.columnName = parts[1]; + break; + case 1: + this.columnName = parts[0]; + break; + default: + throw new RuntimeException("Invalid column name: " + name); + } + } else { + this.columnName = name; + } } public String getTableDelimiter() { @@ -143,6 +196,14 @@ public void setTableDelimiter(String tableDelimiter) { this.tableDelimiter = tableDelimiter; } + public int getOldOracleJoinSyntax() { + return oldOracleJoinSyntax; + } + + public void setOldOracleJoinSyntax(int oldOracleJoinSyntax) { + this.oldOracleJoinSyntax = oldOracleJoinSyntax; + } + @Override public String getFullyQualifiedName() { return getFullyQualifiedName(false); @@ -156,7 +217,9 @@ public String getUnquotedName() { public String getFullyQualifiedName(boolean aliases) { StringBuilder fqn = new StringBuilder(); - if (table != null) { + if (returningQualifier != null) { + fqn.append(returningQualifier); + } else if (table != null) { if (table.getAlias() != null && aliases) { fqn.append(table.getAlias().getName()); } else { @@ -196,6 +259,7 @@ public T accept(ExpressionVisitor expressionVisitor, S context) { @Override public String toString() { return getFullyQualifiedName(true) + + (oldOracleJoinSyntax != SupportsOldOracleJoinSyntax.NO_ORACLE_JOIN ? "(+)" : "") + (commentText != null ? " /* " + commentText + "*/ " : ""); } @@ -219,6 +283,36 @@ public Column withTableDelimiter(String delimiter) { return this; } + public Column withOldOracleJoinSyntax(int oldOracleJoinSyntax) { + this.setOldOracleJoinSyntax(oldOracleJoinSyntax); + return this; + } + + public ReturningReferenceType getReturningReferenceType() { + return returningReferenceType; + } + + public Column setReturningReferenceType(ReturningReferenceType returningReferenceType) { + this.returningReferenceType = returningReferenceType; + return this; + } + + public String getReturningQualifier() { + return returningQualifier; + } + + public Column setReturningQualifier(String returningQualifier) { + this.returningQualifier = returningQualifier; + return this; + } + + public Column withReturningReference(ReturningReferenceType returningReferenceType, + String returningQualifier) { + this.returningReferenceType = returningReferenceType; + this.returningQualifier = returningQualifier; + return this; + } + public String getCommentText() { return commentText; } diff --git a/src/main/java/net/sf/jsqlparser/schema/MultiPartName.java b/src/main/java/net/sf/jsqlparser/schema/MultiPartName.java index e2d985bc1..ce954780d 100644 --- a/src/main/java/net/sf/jsqlparser/schema/MultiPartName.java +++ b/src/main/java/net/sf/jsqlparser/schema/MultiPartName.java @@ -9,10 +9,12 @@ */ package net.sf.jsqlparser.schema; +import java.util.regex.Matcher; import java.util.regex.Pattern; public interface MultiPartName { Pattern LEADING_TRAILING_QUOTES_PATTERN = Pattern.compile("^[\"\\[`]+|[\"\\]`]+$"); + Pattern BACKTICK_PATTERN = Pattern.compile("`([^`]*)`"); /** * Removes leading and trailing quotes from a SQL quoted identifier @@ -27,10 +29,32 @@ static String unquote(String quotedIdentifier) { } static boolean isQuoted(String identifier) { - return identifier!=null && LEADING_TRAILING_QUOTES_PATTERN.matcher(identifier).find(); + return identifier != null && LEADING_TRAILING_QUOTES_PATTERN.matcher(identifier).find(); } String getFullyQualifiedName(); String getUnquotedName(); + + + static String replaceBackticksWithDoubleQuotes(String input) { + if (input == null || input.isEmpty()) { + return input; + } + + Matcher matcher = BACKTICK_PATTERN.matcher(input); + StringBuilder sb = new StringBuilder(); + int lastEnd = 0; + + while (matcher.find()) { + sb.append(input, lastEnd, matcher.start()); // text before match + sb.append('"').append(matcher.group(1)).append('"'); // replace with double quotes + lastEnd = matcher.end(); + } + + sb.append(input.substring(lastEnd)); // append remaining text + return sb.toString(); + } + + } diff --git a/src/main/java/net/sf/jsqlparser/schema/Sequence.java b/src/main/java/net/sf/jsqlparser/schema/Sequence.java index 2f813c1d7..5d7b960cd 100644 --- a/src/main/java/net/sf/jsqlparser/schema/Sequence.java +++ b/src/main/java/net/sf/jsqlparser/schema/Sequence.java @@ -9,13 +9,13 @@ */ package net.sf.jsqlparser.schema; -import net.sf.jsqlparser.parser.ASTNodeAccessImpl; - import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.Optional; +import net.sf.jsqlparser.parser.ASTNodeAccessImpl; /** * Represents the database type for a {@code SEQUENCE} @@ -29,6 +29,7 @@ public class Sequence extends ASTNodeAccessImpl implements MultiPartName { private List partItems = new ArrayList<>(); private List parameters; + private String dataType; public Sequence() {} @@ -45,6 +46,19 @@ public void setParameters(List parameters) { this.parameters = parameters; } + public String getDataType() { + return dataType; + } + + public void setDataType(String dataType) { + this.dataType = dataType; + } + + public Sequence withDataType(String dataType) { + this.setDataType(dataType); + return this; + } + public Database getDatabase() { return new Database(getIndex(DATABASE_IDX)); } @@ -129,6 +143,9 @@ public String getUnquotedName() { @Override public String toString() { StringBuilder sql = new StringBuilder(getFullyQualifiedName()); + if (dataType != null) { + sql.append(" AS ").append(dataType); + } if (parameters != null) { for (Sequence.Parameter parameter : parameters) { sql.append(" ").append(parameter.formatParameter()); @@ -158,10 +175,10 @@ public Sequence addParameters(Collection parameters) { * The available parameters to a sequence */ public enum ParameterType { - INCREMENT_BY, START_WITH, RESTART_WITH, MAXVALUE, NOMAXVALUE, MINVALUE, NOMINVALUE, CYCLE, NOCYCLE, CACHE, NOCACHE, ORDER, NOORDER, KEEP, NOKEEP, SESSION, GLOBAL; + INCREMENT_BY, INCREMENT, START_WITH, START, RESTART_WITH, MAXVALUE, NOMAXVALUE, MINVALUE, NOMINVALUE, CYCLE, NOCYCLE, CACHE, NOCACHE, ORDER, NOORDER, KEEP, NOKEEP, SESSION, GLOBAL; public static ParameterType from(String type) { - return Enum.valueOf(ParameterType.class, type.toUpperCase()); + return Enum.valueOf(ParameterType.class, type.toUpperCase(Locale.ROOT)); } } @@ -189,8 +206,12 @@ public String formatParameter() { switch (option) { case INCREMENT_BY: return prefix("INCREMENT BY"); + case INCREMENT: + return prefix("INCREMENT"); case START_WITH: return prefix("START WITH"); + case START: + return prefix("START"); case RESTART_WITH: if (value != null) { return prefix("RESTART WITH"); diff --git a/src/main/java/net/sf/jsqlparser/schema/Server.java b/src/main/java/net/sf/jsqlparser/schema/Server.java index 9ac9bd2d2..7e4eaa51c 100644 --- a/src/main/java/net/sf/jsqlparser/schema/Server.java +++ b/src/main/java/net/sf/jsqlparser/schema/Server.java @@ -14,7 +14,7 @@ public final class Server implements MultiPartName { public static final Pattern SERVER_PATTERN = - Pattern.compile("\\[([^\\]]+?)(?:\\\\([^\\]]+))?\\]"); + Pattern.compile("\\[([^\\]\\\\]+)(?:\\\\([^\\]]+))?\\]"); private String serverName; diff --git a/src/main/java/net/sf/jsqlparser/schema/Table.java b/src/main/java/net/sf/jsqlparser/schema/Table.java index cd0aa679d..1de14a8a5 100644 --- a/src/main/java/net/sf/jsqlparser/schema/Table.java +++ b/src/main/java/net/sf/jsqlparser/schema/Table.java @@ -170,7 +170,49 @@ public String getUnquotedSchemaName() { } public Table setSchemaName(String schemaName) { - this.setIndex(SCHEMA_IDX, schemaName); + if (schemaName == null) { + setIndex(SCHEMA_IDX, null); + return this; + } + + // BigQuery seems to allow things like: `catalogName.schemaName.tableName` in only one pair + // of quotes + // however, some people believe that Dots in Names are a good idea, so provide a switch-off + boolean splitNamesOnDelimiter = System.getProperty("SPLIT_NAMES_ON_DELIMITER") == null || + !List + .of("0", "N", "n", "FALSE", "false", "OFF", "off") + .contains(System.getProperty("SPLIT_NAMES_ON_DELIMITER")); + + if (MultiPartName.isQuoted(schemaName) && schemaName.contains(".") + && splitNamesOnDelimiter) { + String[] parts = MultiPartName.unquote(schemaName).split("\\."); + switch (parts.length) { + case 2: + setIndex(DATABASE_IDX, "\"" + parts[0] + "\""); + setIndex(SCHEMA_IDX, "\"" + parts[1] + "\""); + break; + case 1: + setIndex(SCHEMA_IDX, "\"" + parts[0] + "\""); + break; + default: + throw new RuntimeException("Invalid schema name: " + schemaName); + } + } else if (schemaName.contains(".") && splitNamesOnDelimiter) { + String[] parts = MultiPartName.unquote(schemaName).split("\\."); + switch (parts.length) { + case 2: + setIndex(DATABASE_IDX, parts[0]); + setIndex(SCHEMA_IDX, parts[1]); + break; + case 1: + setIndex(SCHEMA_IDX, parts[0]); + break; + default: + throw new RuntimeException("Invalid schema name: " + schemaName); + } + } else { + this.setIndex(SCHEMA_IDX, schemaName); + } return this; } @@ -260,6 +302,10 @@ public Table setTimeTravelStrAfterAlias(String timeTravelStrAfterAlias) { return this; } + public void setNameParts(List nameParts) { + this.partItems = nameParts; + } + private void setIndex(int idx, String value) { int size = partItems.size(); for (int i = 0; i < idx - size + 1; i++) { diff --git a/src/main/java/net/sf/jsqlparser/statement/DBMSType.java b/src/main/java/net/sf/jsqlparser/statement/DBMSType.java index 4b3d667da..4f2500e9c 100644 --- a/src/main/java/net/sf/jsqlparser/statement/DBMSType.java +++ b/src/main/java/net/sf/jsqlparser/statement/DBMSType.java @@ -9,6 +9,7 @@ */ package net.sf.jsqlparser.statement; +import java.util.Locale; import net.sf.jsqlparser.expression.StringValue; public class DBMSType implements SourceDestinationType { @@ -20,7 +21,7 @@ public DBMSType(String dbmsType) { } public DBMSType(String dbmsType, String jdbcDriverDefinition) { - this.dbmsType = Kind.valueOf(dbmsType.toUpperCase()); + this.dbmsType = Kind.valueOf(dbmsType.toUpperCase(Locale.ROOT)); if (jdbcDriverDefinition != null) { this.jdbcDriverDefinition = new StringValue(jdbcDriverDefinition); } diff --git a/src/main/java/net/sf/jsqlparser/statement/DeclareType.java b/src/main/java/net/sf/jsqlparser/statement/DeclareType.java index 5208a333a..cb8cdbc46 100644 --- a/src/main/java/net/sf/jsqlparser/statement/DeclareType.java +++ b/src/main/java/net/sf/jsqlparser/statement/DeclareType.java @@ -9,6 +9,8 @@ */ package net.sf.jsqlparser.statement; +import java.util.Locale; + /** * @author tobens */ @@ -16,6 +18,6 @@ public enum DeclareType { TABLE, AS, TYPE; public static DeclareType from(String type) { - return Enum.valueOf(DeclareType.class, type.toUpperCase()); + return Enum.valueOf(DeclareType.class, type.toUpperCase(Locale.ROOT)); } } diff --git a/src/main/java/net/sf/jsqlparser/statement/ExplainStatement.java b/src/main/java/net/sf/jsqlparser/statement/ExplainStatement.java index 048356425..fbc5e8503 100644 --- a/src/main/java/net/sf/jsqlparser/statement/ExplainStatement.java +++ b/src/main/java/net/sf/jsqlparser/statement/ExplainStatement.java @@ -12,17 +12,16 @@ import java.io.Serializable; import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; import java.util.stream.Collectors; - import net.sf.jsqlparser.schema.Table; -import net.sf.jsqlparser.statement.select.Select; /** * An {@code EXPLAIN} statement */ public class ExplainStatement implements Statement { private String keyword; - private Select select; + private Statement statement; private LinkedHashMap options; private Table table; @@ -37,24 +36,17 @@ public ExplainStatement() { public ExplainStatement(String keyword, Table table) { this.keyword = keyword; this.table = table; - this.select = null; } - public ExplainStatement(String keyword, Select select, List